diff --git a/common/test/acceptance/tests/lms/test_teams.py b/common/test/acceptance/tests/lms/test_teams.py index b8ba0105aa..9d7376d11c 100644 --- a/common/test/acceptance/tests/lms/test_teams.py +++ b/common/test/acceptance/tests/lms/test_teams.py @@ -7,7 +7,6 @@ import time from dateutil.parser import parse import ddt -from flaky import flaky from nose.plugins.attrib import attr from uuid import uuid4 from unittest import skip @@ -286,6 +285,14 @@ class MyTeamsTest(TeamsTabBase): self.topic = {u"name": u"Example Topic", u"id": "example_topic", u"description": "Description"} self.set_team_configuration({'course_id': self.course_id, 'max_team_size': 10, 'topics': [self.topic]}) self.my_teams_page = MyTeamsPage(self.browser, self.course_id) + self.page_viewed_event = { + 'event_type': 'edx.team.page_viewed', + 'event': { + 'page_name': 'my-teams', + 'topic_id': None, + 'team_id': None + } + } def test_not_member_of_any_teams(self): """ @@ -295,7 +302,8 @@ class MyTeamsTest(TeamsTabBase): And I should see no teams And I should see a message that I belong to no teams. """ - self.my_teams_page.visit() + with self.assert_events_match_during(self.only_team_events, expected_events=[self.page_viewed_event]): + self.my_teams_page.visit() self.assertEqual(len(self.my_teams_page.team_cards), 0, msg='Expected to see no team cards') self.assertEqual( self.my_teams_page.q(css='.page-content-main').text, @@ -313,7 +321,8 @@ class MyTeamsTest(TeamsTabBase): """ teams = self.create_teams(self.topic, 1) self.create_membership(self.user_info['username'], teams[0]['id']) - self.my_teams_page.visit() + with self.assert_events_match_during(self.only_team_events, expected_events=[self.page_viewed_event]): + self.my_teams_page.visit() self.verify_teams(self.my_teams_page, teams) @@ -511,6 +520,28 @@ class BrowseTopicsTest(TeamsTabBase): self.assertEqual(browse_teams_page.header_name, 'Example Topic') self.assertEqual(browse_teams_page.header_description, 'Description') + def test_page_viewed_event(self): + """ + Scenario: Visiting the browse topics page should fire a page viewed event. + Given I am enrolled in a course with a team configuration and a topic + When I visit the browse topics page + Then my browser should post a page viewed event + """ + topic = {u"name": u"Example Topic", u"id": u"example_topic", u"description": "Description"} + self.set_team_configuration( + {u"max_team_size": 1, u"topics": [topic]} + ) + events = [{ + 'event_type': 'edx.team.page_viewed', + 'event': { + 'page_name': 'browse', + 'topic_id': None, + 'team_id': None + } + }] + with self.assert_events_match_during(self.only_team_events, expected_events=events): + self.topics_page.visit() + @attr('shard_5') @ddt.ddt @@ -752,6 +783,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): Then I should see the search result page And the search header should be shown And 0 results should be shown + And my browser should fire a page viewed event for the search page """ # Note: all searches will return 0 results with the mock search server # used by Bok Choy. @@ -766,12 +798,38 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): 'topic_id': self.topic['id'], 'number_of_results': 0 } + }, { + 'event_type': 'edx.team.page_viewed', + 'event': { + 'page_name': 'search-teams', + 'topic_id': self.topic['id'], + 'team_id': None + } }] with self.assert_events_match_during(self.only_team_events, expected_events=events): search_results_page = self.browse_teams_page.search(search_text) self.verify_search_header(search_results_page, search_text) self.assertTrue(search_results_page.get_pagination_header_text().startswith('Showing 0 out of 0 total')) + def test_page_viewed_event(self): + """ + Scenario: Visiting the browse page should fire a page viewed event. + Given I am enrolled in a course with a team configuration and a topic + When I visit the Teams page + Then my browser should post a page viewed event for the teams page + """ + self.create_teams(self.topic, 5) + events = [{ + 'event_type': 'edx.team.page_viewed', + 'event': { + 'page_name': 'single-topic', + 'topic_id': self.topic['id'], + 'team_id': None + } + }] + with self.assert_events_match_during(self.only_team_events, expected_events=events): + self.browse_teams_page.visit() + @attr('shard_5') class TeamFormActions(TeamsTabBase): @@ -1024,6 +1082,24 @@ class CreateTeamTest(TeamFormActions): self.verify_my_team_count(0) + def test_page_viewed_event(self): + """ + Scenario: Visiting the create team page should fire a page viewed event. + Given I am enrolled in a course with a team configuration and a topic + When I visit the create team page + Then my browser should post a page viewed event + """ + events = [{ + 'event_type': 'edx.team.page_viewed', + 'event': { + 'page_name': 'new-team', + 'topic_id': self.topic['id'], + 'team_id': None + } + }] + with self.assert_events_match_during(self.only_team_events, expected_events=events): + self.verify_and_navigate_to_create_team_page() + @ddt.ddt class EditTeamTest(TeamFormActions): @@ -1224,6 +1300,24 @@ class EditTeamTest(TeamFormActions): language='English' ) + def test_page_viewed_event(self): + """ + Scenario: Visiting the edit team page should fire a page viewed event. + Given I am enrolled in a course with a team configuration and a topic + When I visit the edit team page + Then my browser should post a page viewed event + """ + events = [{ + 'event_type': 'edx.team.page_viewed', + 'event': { + 'page_name': 'edit-team', + 'topic_id': self.topic['id'], + 'team_id': self.team['id'] + } + }] + with self.assert_events_match_during(self.only_team_events, expected_events=events): + self.verify_and_navigate_to_edit_team_page() + @attr('shard_5') @ddt.ddt @@ -1554,3 +1648,22 @@ class TeamPageTest(TeamsTabBase): # Verify that if one switches to "My Team" without reloading the page, the old team no longer shows. self.teams_page.click_all_topics() self.verify_my_team_count(0) + + def test_page_viewed_event(self): + """ + Scenario: Visiting the team profile page should fire a page viewed event. + Given I am enrolled in a course with a team configuration and a topic + When I visit the team profile page + Then my browser should post a page viewed event + """ + self._set_team_configuration_and_membership() + events = [{ + 'event_type': 'edx.team.page_viewed', + 'event': { + 'page_name': 'single-team', + 'topic_id': self.topic['id'], + 'team_id': self.teams[0]['id'] + } + }] + with self.assert_events_match_during(self.only_team_events, expected_events=events): + self.team_page.visit() diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js index 922db6b5f5..f95dd350e5 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js @@ -1,10 +1,12 @@ define([ 'jquery', 'backbone', + 'logger', 'common/js/spec_helpers/ajax_helpers', + 'common/js/spec_helpers/spec_helpers', 'teams/js/views/teams_tab', 'teams/js/spec_helpers/team_spec_helpers' -], function ($, Backbone, AjaxHelpers, TeamsTabView, TeamSpecHelpers) { +], function ($, Backbone, Logger, AjaxHelpers, SpecHelpers, TeamsTabView, TeamSpecHelpers) { 'use strict'; describe('TeamsTab', function () { @@ -34,14 +36,34 @@ define([ return teamsTabView; }; + /** + * Filters out all team events from a list of requests. + */ + var removeTeamEvents = function (requests) { + return requests.filter(function (request) { + if (request.requestBody && request.requestBody.startsWith('event_type=edx.team')) { + return false; + } else { + return true; + } + }); + }; + beforeEach(function () { setFixtures('
'); spyOn($.fn, 'focus'); + spyOn(Logger, 'log'); }); afterEach(Backbone.history.stop); describe('Navigation', function () { + it('does not interfere with anchor links to #content', function () { + var teamsTabView = createTeamsTabView(); + teamsTabView.router.navigate('#content', {trigger: true}); + expect(teamsTabView.$('.warning')).toHaveClass('is-hidden'); + }); + it('displays and focuses an error message when trying to navigate to a nonexistent page', function () { var teamsTabView = createTeamsTabView(); teamsTabView.router.navigate('no_such_page', {trigger: true}); @@ -49,12 +71,6 @@ define([ expectFocus(teamsTabView.$('.warning')); }); - it('does not interfere with anchor links to #content', function () { - var teamsTabView = createTeamsTabView(); - teamsTabView.router.navigate('#content', {trigger: true}); - expect(teamsTabView.$('.warning')).toHaveClass('is-hidden'); - }); - it('displays and focuses an error message when trying to navigate to a nonexistent topic', function () { var requests = AjaxHelpers.requests(this), teamsTabView = createTeamsTabView(); @@ -94,6 +110,64 @@ define([ }); }); + describe('Analytics Events', function () { + SpecHelpers.withData({ + 'fires a page view event for the topic page': [ + 'topics/' + TeamSpecHelpers.testTopicID, + { + page_name: 'single-topic', + topic_id: TeamSpecHelpers.testTopicID, + team_id: null + } + ], + 'fires a page view event for the team page': [ + 'teams/' + TeamSpecHelpers.testTopicID + '/test_team_id', + { + page_name: 'single-team', + topic_id: TeamSpecHelpers.testTopicID, + team_id: 'test_team_id' + } + ], + 'fires a page view event for the search team page': [ + 'topics/' + TeamSpecHelpers.testTopicID + '/search', + { + page_name: 'search-teams', + topic_id: TeamSpecHelpers.testTopicID, + team_id: null + } + ], + 'fires a page view event for the new team page': [ + 'topics/' + TeamSpecHelpers.testTopicID + '/create-team', + { + page_name: 'new-team', + topic_id: TeamSpecHelpers.testTopicID, + team_id: null + } + ], + 'fires a page view event for the edit team page': [ + 'topics/' + TeamSpecHelpers.testTopicID + '/' + 'test_team_id/edit-team', + { + page_name: 'edit-team', + topic_id: TeamSpecHelpers.testTopicID, + team_id: 'test_team_id' + } + ] + }, function (url, expectedEvent) { + if (url.indexOf('search') !== -1 + || url.indexOf('create-team') !== -1 + || url.indexOf('edit-team') !== -1) { + debugger; + } + var requests = AjaxHelpers.requests(this), + teamsTabView = createTeamsTabView(); + teamsTabView.router.navigate(url, {trigger: true}); + if (requests.length) { + AjaxHelpers.respondWithJson(requests, {}); + } + expect(Logger.log).toHaveBeenCalledWith('edx.team.page_viewed', expectedEvent); + }); + }); + describe('Discussion privileges', function () { it('allows privileged access to any team', function () { var teamsTabView = createTeamsTabView({ @@ -206,7 +280,7 @@ define([ // Navigate back to the teams list teamsTabView.$('.breadcrumbs a').last().click(); - verifyTeamsRequest(requests, { + verifyTeamsRequest(removeTeamEvents(requests), { order_by: 'last_activity_at', text_search: '' }); diff --git a/lms/djangoapps/teams/static/teams/js/utils/team_analytics.js b/lms/djangoapps/teams/static/teams/js/utils/team_analytics.js new file mode 100644 index 0000000000..c0ae5a0113 --- /dev/null +++ b/lms/djangoapps/teams/static/teams/js/utils/team_analytics.js @@ -0,0 +1,23 @@ +/** + * Utility methods for emitting teams events. See the event spec: + * https://openedx.atlassian.net/wiki/display/AN/Teams+Feature+Event+Design + */ +;(function (define) { + 'use strict'; + + define([ + 'logger' + ], function (Logger) { + var TeamAnalytics = { + emitPageViewed: function (page_name, topic_id, team_id) { + Logger.log('edx.team.page_viewed', { + page_name: page_name, + topic_id: topic_id, + team_id: team_id + }); + } + }; + + return TeamAnalytics; + }); +}).call(this, define || RequireJS.define); diff --git a/lms/djangoapps/teams/static/teams/js/views/teams_tab.js b/lms/djangoapps/teams/static/teams/js/views/teams_tab.js index 21becabd75..27719a5e52 100644 --- a/lms/djangoapps/teams/static/teams/js/views/teams_tab.js +++ b/lms/djangoapps/teams/static/teams/js/views/teams_tab.js @@ -7,12 +7,13 @@ 'common/js/components/views/search_field', 'js/components/header/views/header', 'js/components/header/models/header', - 'js/components/tabbed/views/tabbed_view', 'teams/js/models/topic', 'teams/js/collections/topic', 'teams/js/models/team', 'teams/js/collections/team', 'teams/js/collections/team_membership', + 'teams/js/utils/team_analytics', + 'teams/js/views/teams_tabbed_view', 'teams/js/views/topics', 'teams/js/views/team_profile', 'teams/js/views/my_teams', @@ -21,9 +22,9 @@ 'teams/js/views/team_profile_header_actions', 'teams/js/views/team_utils', 'text!teams/templates/teams_tab.underscore'], - function (Backbone, _, gettext, SearchFieldView, HeaderView, HeaderModel, TabbedView, - TopicModel, TopicCollection, TeamModel, TeamCollection, TeamMembershipCollection, - TopicsView, TeamProfileView, MyTeamsView, TopicTeamsView, TeamEditView, + function (Backbone, _, gettext, SearchFieldView, HeaderView, HeaderModel, + TopicModel, TopicCollection, TeamModel, TeamCollection, TeamMembershipCollection, TeamAnalytics, + TeamsTabbedView, TopicsView, TeamProfileView, MyTeamsView, TopicTeamsView, TeamEditView, TeamProfileHeaderActionsView, TeamUtils, teamsTemplate) { var TeamsHeaderModel = HeaderModel.extend({ initialize: function () { @@ -118,7 +119,7 @@ this.mainView = this.tabbedView = this.createViewWithHeader({ title: gettext("Teams"), description: gettext("See all teams in your course, organized by topic. Join a team to collaborate with other learners who are interested in the same topic as you are."), - mainView: new TabbedView({ + mainView: new TeamsTabbedView({ tabs: [{ title: gettext('My Team'), url: 'my-teams', @@ -180,6 +181,7 @@ this.getTeamsView(topicID).done(function (teamsView) { self.teamsView = self.mainView = teamsView; self.render(); + TeamAnalytics.emitPageViewed('single-topic', topicID, null); }); }, @@ -205,6 +207,7 @@ showSortControls: false }); view.render(); + TeamAnalytics.emitPageViewed('search-teams', topicID, null); }); } }, @@ -227,6 +230,7 @@ }) }); view.render(); + TeamAnalytics.emitPageViewed('new-team', topicID, null); }); }, @@ -254,6 +258,7 @@ }); self.mainView = editViewWithHeader; self.render(); + TeamAnalytics.emitPageViewed('edit-team', topicID, teamID); }); }); }, @@ -340,6 +345,7 @@ this.getBrowseTeamView(topicID, teamID).done(function (browseTeamView) { self.mainView = browseTeamView; self.render(); + TeamAnalytics.emitPageViewed('single-team', topicID, teamID); }); }, diff --git a/lms/djangoapps/teams/static/teams/js/views/teams_tabbed_view.js b/lms/djangoapps/teams/static/teams/js/views/teams_tabbed_view.js new file mode 100644 index 0000000000..94c16f049d --- /dev/null +++ b/lms/djangoapps/teams/static/teams/js/views/teams_tabbed_view.js @@ -0,0 +1,24 @@ +/** + * A custom TabbedView for Teams. + */ +;(function (define) { + 'use strict'; + + define([ + 'js/components/tabbed/views/tabbed_view', + 'teams/js/utils/team_analytics' + ], function (TabbedView, TeamAnalytics) { + var TeamsTabbedView = TabbedView.extend({ + /** + * Overrides TabbedView.prototype.setActiveTab in order to + * log page viewed events. + */ + setActiveTab: function (index) { + TabbedView.prototype.setActiveTab.call(this, index); + TeamAnalytics.emitPageViewed(this.getTabMeta(index).tab.url, null, null); + } + }); + + return TeamsTabbedView; + }); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/components/tabbed/views/tabbed_view.js b/lms/static/js/components/tabbed/views/tabbed_view.js index 9258f9cd0b..e0de186cc5 100644 --- a/lms/static/js/components/tabbed/views/tabbed_view.js +++ b/lms/static/js/components/tabbed/views/tabbed_view.js @@ -59,16 +59,10 @@ }, setActiveTab: function (index) { - var tab, tabEl, view; - if (typeof index === 'string') { - tab = this.urlMap[index]; - tabEl = this.$('a[data-url='+index+']'); - } - else { - tab = this.tabs[index]; - tabEl = this.$('a[data-index='+index+']'); - } - view = tab.view; + var tabMeta = this.getTabMeta(index), + tab = tabMeta.tab, + tabEl = tabMeta.element, + view = tab.view; this.$('a.is-active').removeClass('is-active').attr('aria-selected', 'false'); tabEl.addClass('is-active').attr('aria-selected', 'true'); view.setElement(this.$('.page-content-main')).render(); @@ -81,6 +75,22 @@ switchTab: function (event) { event.preventDefault(); this.setActiveTab($(event.currentTarget).data('index')); + }, + + /** + * Get the tab by name or index. Returns an object + * encapsulating the tab object and its element. + */ + getTabMeta: function (tabNameOrIndex) { + var tab, element; + if (typeof tabNameOrIndex === 'string') { + tab = this.urlMap[tabNameOrIndex]; + element = this.$('a[data-url='+tabNameOrIndex+']'); + } else { + tab = this.tabs[tabNameOrIndex]; + element = this.$('a[data-index='+tabNameOrIndex+']'); + } + return {'tab': tab, 'element': element}; } }); return TabbedView;