From a743f4a642d28ca5d294b8d5b655ad26265416ef Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Thu, 3 Sep 2015 11:30:24 -0400 Subject: [PATCH] Add teams eventing and tests. --- .../test/acceptance/tests/lms/test_teams.py | 56 ++++++++++++++++-- lms/djangoapps/teams/tests/test_views.py | 59 +++++++++++++++++-- lms/djangoapps/teams/views.py | 32 ++++++++++ 3 files changed, 137 insertions(+), 10 deletions(-) diff --git a/common/test/acceptance/tests/lms/test_teams.py b/common/test/acceptance/tests/lms/test_teams.py index 11366b142b..ffb67ae157 100644 --- a/common/test/acceptance/tests/lms/test_teams.py +++ b/common/test/acceptance/tests/lms/test_teams.py @@ -11,7 +11,7 @@ from flaky import flaky from nose.plugins.attrib import attr from uuid import uuid4 -from ..helpers import UniqueCourseTest +from ..helpers import UniqueCourseTest, EventsTestMixin from ...fixtures import LMS_BASE_URL from ...fixtures.course import CourseFixture from ...fixtures.discussion import ( @@ -28,7 +28,7 @@ from ...pages.lms.teams import TeamsPage, MyTeamsPage, BrowseTopicsPage, BrowseT TOPICS_PER_PAGE = 12 -class TeamsTabBase(UniqueCourseTest): +class TeamsTabBase(EventsTestMixin, UniqueCourseTest): """Base class for Teams Tab tests""" def setUp(self): super(TeamsTabBase, self).setUp() @@ -123,6 +123,10 @@ class TeamsTabBase(UniqueCourseTest): # We are doing these operations on this top-level page object to avoid reloading the page. self.teams_page.verify_my_team_count(expected_number_of_teams) + def only_team_events(self, event): + """Filter out all non-team events.""" + return event['event_type'].startswith('edx.team.') + @ddt.ddt @attr('shard_5') @@ -899,7 +903,8 @@ class CreateTeamTest(TeamFormActions): Then I should see the Create Team header and form When I fill all the fields present with appropriate data And I click Create button - Then I should see the page for my team + Then I expect analytics events to be emitted + And I should see the page for my team And I should see the message that says "You are member of this team" And the new team should be added to the list of teams within the topic And the number of teams should be updated on the topic card @@ -911,7 +916,24 @@ class CreateTeamTest(TeamFormActions): self.verify_and_navigate_to_create_team_page() self.fill_create_or_edit_form() - self.create_or_edit_team_page.submit_form() + + expected_events = [ + { + 'event_type': 'edx.team.created', + 'event': { + 'course_id': self.course_id, + } + }, + { + 'event_type': 'edx.team.learner_added', + 'event': { + 'course_id': self.course_id, + 'add_method': 'added_on_create', + } + } + ] + with self.assert_events_match_during(event_filter=self.only_team_events, expected_events=expected_events): + self.create_or_edit_team_page.submit_form() # Verify that the page is shown for the new team team_page = TeamPage(self.browser, self.course_id) @@ -1330,6 +1352,7 @@ class TeamPageTest(TeamsTabBase): And I should not see New Post button When I click on Join Team button Then there should be no Join Team button and no message + And an analytics event should be emitted And I should see the updated information under Team Details And I should see New Post button And if I switch to "My Team", the team I have joined is displayed @@ -1337,7 +1360,17 @@ class TeamPageTest(TeamsTabBase): self._set_team_configuration_and_membership(create_membership=False) self.team_page.visit() self.assertTrue(self.team_page.join_team_button_present) - self.team_page.click_join_team_button() + expected_events = [ + { + 'event_type': 'edx.team.learner_added', + 'event': { + 'course_id': self.course_id, + 'add_method': 'joined_from_team_view' + } + } + ] + with self.assert_events_match_during(event_filter=self.only_team_events, expected_events=expected_events): + self.team_page.click_join_team_button() self.assertFalse(self.team_page.join_team_button_present) self.assertFalse(self.team_page.join_team_message_present) self.assert_team_details(num_members=1, is_member=True) @@ -1397,6 +1430,7 @@ class TeamPageTest(TeamsTabBase): Then I should see Leave Team link When I click on Leave Team link Then user should be removed from team + And an analytics event should be emitted And I should see Join Team button And I should not see New Post button And if I switch to "My Team", the team I have left is not displayed @@ -1405,7 +1439,17 @@ class TeamPageTest(TeamsTabBase): self.team_page.visit() self.assertFalse(self.team_page.join_team_button_present) self.assert_team_details(num_members=1) - self.team_page.click_leave_team_link() + expected_events = [ + { + 'event_type': 'edx.team.learner_removed', + 'event': { + 'course_id': self.course_id, + 'remove_method': 'self_removal' + } + } + ] + with self.assert_events_match_during(event_filter=self.only_team_events, expected_events=expected_events): + self.team_page.click_leave_team_link() self.assert_team_details(num_members=0, is_member=False) self.assertTrue(self.team_page.join_team_button_present) diff --git a/lms/djangoapps/teams/tests/test_views.py b/lms/djangoapps/teams/tests/test_views.py index 2e43659bda..07975309d7 100644 --- a/lms/djangoapps/teams/tests/test_views.py +++ b/lms/djangoapps/teams/tests/test_views.py @@ -14,6 +14,7 @@ from rest_framework.test import APITestCase, APIClient from courseware.tests.factories import StaffFactory from common.test.utils import skip_signal +from util.testing import EventTestMixin from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory from student.models import CourseEnrollment from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase @@ -529,9 +530,12 @@ class TestListTeamsAPI(TeamAPITestCase): @ddt.ddt -class TestCreateTeamAPI(TeamAPITestCase): +class TestCreateTeamAPI(EventTestMixin, TeamAPITestCase): """Test cases for the team creation endpoint.""" + def setUp(self): # pylint: disable=arguments-differ + super(TestCreateTeamAPI, self).setUp('teams.views.tracker') + @ddt.data( (None, 401), ('student_inactive', 401), @@ -549,11 +553,15 @@ class TestCreateTeamAPI(TeamAPITestCase): teams = self.get_teams_list(user=user) self.assertIn("New Team", [team['name'] for team in teams['results']]) + def _expected_team_id(self, team, expected_prefix): + """ Return the team id that we'd expect given this team data and this prefix. """ + return expected_prefix + '-' + team['discussion_topic_id'] + def verify_expected_team_id(self, team, expected_prefix): """ Verifies that the team id starts with the specified prefix and ends with the discussion_topic_id """ self.assertIn('id', team) self.assertIn('discussion_topic_id', team) - self.assertEqual(team['id'], expected_prefix + '-' + team['discussion_topic_id']) + self.assertEqual(team['id'], self._expected_team_id(team, expected_prefix)) def test_naming(self): new_teams = [ @@ -640,6 +648,19 @@ class TestCreateTeamAPI(TeamAPITestCase): self.verify_expected_team_id(team, 'fully-specified-team') del team['id'] + self.assert_event_emitted( + 'edx.team.created', + team_id=self._expected_team_id(team, 'fully-specified-team'), + course_id=unicode(self.test_course_1.id), + ) + + self.assert_event_emitted( + 'edx.team.learner_added', + team_id=self._expected_team_id(team, 'fully-specified-team'), + course_id=unicode(self.test_course_1.id), + user_id=self.users[creator].id, + add_method='added_on_create' + ) # Remove date_created and discussion_topic_id because they change between test runs del team['date_created'] del team['discussion_topic_id'] @@ -1026,9 +1047,12 @@ class TestListMembershipAPI(TeamAPITestCase): @ddt.ddt -class TestCreateMembershipAPI(TeamAPITestCase): +class TestCreateMembershipAPI(EventTestMixin, TeamAPITestCase): """Test cases for the membership creation endpoint.""" + def setUp(self): # pylint: disable=arguments-differ + super(TestCreateMembershipAPI, self).setUp('teams.views.tracker') + @ddt.data( (None, 401), ('student_inactive', 401), @@ -1053,6 +1077,18 @@ class TestCreateMembershipAPI(TeamAPITestCase): memberships = self.get_membership_list(200, {'team_id': self.solar_team.team_id}) self.assertEqual(memberships['count'], 2) + add_method = 'joined_from_team_view' if user == 'student_enrolled_not_on_team' else 'added_by_another_user' + + self.assert_event_emitted( + 'edx.team.learner_added', + team_id=self.solar_team.team_id, + user_id=self.users['student_enrolled_not_on_team'].id, + course_id=unicode(self.solar_team.course_id), + add_method=add_method + ) + else: + self.assert_no_events_were_emitted() + def test_no_username(self): response = self.post_create_membership(400, {'team_id': self.solar_team.team_id}) self.assertIn('username', json.loads(response.content)['field_errors']) @@ -1176,9 +1212,12 @@ class TestDetailMembershipAPI(TeamAPITestCase): @ddt.ddt -class TestDeleteMembershipAPI(TeamAPITestCase): +class TestDeleteMembershipAPI(EventTestMixin, TeamAPITestCase): """Test cases for the membership deletion endpoint.""" + def setUp(self): # pylint: disable=arguments-differ + super(TestDeleteMembershipAPI, self).setUp('teams.views.tracker') + @ddt.data( (None, 401), ('student_inactive', 401), @@ -1198,6 +1237,18 @@ class TestDeleteMembershipAPI(TeamAPITestCase): user=user ) + if status == 204: + remove_method = 'self_removal' if user == 'student_enrolled' else 'removed_by_admin' + self.assert_event_emitted( + 'edx.team.learner_removed', + team_id=self.solar_team.team_id, + course_id=unicode(self.solar_team.course_id), + user_id=self.users['student_enrolled'].id, + remove_method=remove_method + ) + else: + self.assert_no_events_were_emitted() + def test_bad_team(self): self.delete_membership('no_such_team', self.users['student_enrolled'].username, 404) diff --git a/lms/djangoapps/teams/views.py b/lms/djangoapps/teams/views.py index 9bcb85ea3f..4ceda4e9fa 100644 --- a/lms/djangoapps/teams/views.py +++ b/lms/djangoapps/teams/views.py @@ -34,6 +34,7 @@ from xmodule.modulestore.django import modulestore from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey +from eventtracking import tracker from courseware.courses import get_course_with_access, has_access from student.models import CourseEnrollment, CourseAccessRole from student.roles import CourseStaffRole @@ -417,9 +418,22 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView): }, status=status.HTTP_400_BAD_REQUEST) else: team = serializer.save() + tracker.emit('edx.team.created', { + 'team_id': team.team_id, + 'course_id': unicode(course_id) + }) if not team_administrator: # Add the creating user to the team. team.add_user(request.user) + tracker.emit( + 'edx.team.learner_added', + { + 'team_id': team.team_id, + 'user_id': request.user.id, + 'course_id': unicode(team.course_id), + 'add_method': 'added_on_create' + } + ) return Response(CourseTeamSerializer(team).data) def get_page(self): @@ -974,6 +988,15 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView): try: membership = team.add_user(user) + tracker.emit( + 'edx.team.learner_added', + { + 'team_id': team.team_id, + 'user_id': user.id, + 'course_id': unicode(team.course_id), + 'add_method': 'joined_from_team_view' if user == request.user else 'added_by_another_user' + } + ) except AlreadyOnTeamInCourse: return Response( build_api_error( @@ -1100,6 +1123,15 @@ class MembershipDetailView(ExpandableFieldViewMixin, GenericAPIView): if has_team_api_access(request.user, team.course_id, access_username=username): membership = self.get_membership(username, team) membership.delete() + tracker.emit( + 'edx.team.learner_removed', + { + 'team_id': team.team_id, + 'course_id': unicode(team.course_id), + 'user_id': membership.user.id, + 'remove_method': 'self_removal' if membership.user == request.user else 'removed_by_admin' + } + ) return Response(status=status.HTTP_204_NO_CONTENT) else: return Response(status=status.HTTP_404_NOT_FOUND)