Add events for tracking when teams are edited.
TNL-3190
This commit is contained in:
@@ -99,6 +99,35 @@ def emit_field_changed_events(instance, user, db_table, excluded_fields=None, hi
|
||||
del instance._changed_fields
|
||||
|
||||
|
||||
def truncate_fields(old_value, new_value):
|
||||
"""
|
||||
Truncates old_value and new_value for analytics event emission if necessary.
|
||||
|
||||
Args:
|
||||
old_value(obj): the value before the change
|
||||
new_value(obj): the new value being saved
|
||||
|
||||
Returns:
|
||||
a dictionary with the following fields:
|
||||
'old': the truncated old value
|
||||
'new': the truncated new value
|
||||
'truncated': the list of fields that have been truncated
|
||||
"""
|
||||
# Compute the maximum value length so that two copies can fit into the maximum event size
|
||||
# in addition to all the other fields recorded.
|
||||
max_value_length = settings.TRACK_MAX_EVENT / 4
|
||||
|
||||
serialized_old_value, old_was_truncated = _get_truncated_setting_value(old_value, max_length=max_value_length)
|
||||
serialized_new_value, new_was_truncated = _get_truncated_setting_value(new_value, max_length=max_value_length)
|
||||
truncated_values = []
|
||||
if old_was_truncated:
|
||||
truncated_values.append("old")
|
||||
if new_was_truncated:
|
||||
truncated_values.append("new")
|
||||
|
||||
return {'old': serialized_old_value, 'new': serialized_new_value, 'truncated': truncated_values}
|
||||
|
||||
|
||||
def emit_setting_changed_event(user, db_table, setting_name, old_value, new_value):
|
||||
"""Emits an event for a change in a setting.
|
||||
|
||||
@@ -112,27 +141,15 @@ def emit_setting_changed_event(user, db_table, setting_name, old_value, new_valu
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# Compute the maximum value length so that two copies can fit into the maximum event size
|
||||
# in addition to all the other fields recorded.
|
||||
max_value_length = settings.TRACK_MAX_EVENT / 4
|
||||
truncated_fields = truncate_fields(old_value, new_value)
|
||||
|
||||
truncated_fields['setting'] = setting_name
|
||||
truncated_fields['user_id'] = user.id
|
||||
truncated_fields['table'] = db_table
|
||||
|
||||
serialized_old_value, old_was_truncated = _get_truncated_setting_value(old_value, max_length=max_value_length)
|
||||
serialized_new_value, new_was_truncated = _get_truncated_setting_value(new_value, max_length=max_value_length)
|
||||
truncated_values = []
|
||||
if old_was_truncated:
|
||||
truncated_values.append("old")
|
||||
if new_was_truncated:
|
||||
truncated_values.append("new")
|
||||
tracker.emit(
|
||||
USER_SETTINGS_CHANGED_EVENT_NAME,
|
||||
{
|
||||
"setting": setting_name,
|
||||
"old": serialized_old_value,
|
||||
"new": serialized_new_value,
|
||||
"truncated": truncated_values,
|
||||
"user_id": user.id,
|
||||
"table": db_table,
|
||||
}
|
||||
truncated_fields
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1017,6 +1017,7 @@ class EditTeamTest(TeamFormActions):
|
||||
Then I should see the Edit Team button
|
||||
And When I click edit team button
|
||||
Then I should see the edit team page
|
||||
And an analytics event should be fired
|
||||
When I edit all the fields with appropriate data
|
||||
And I click Update button
|
||||
Then I should see the page for my team with updated data
|
||||
@@ -1030,7 +1031,55 @@ class EditTeamTest(TeamFormActions):
|
||||
self.verify_and_navigate_to_edit_team_page()
|
||||
|
||||
self.fill_create_or_edit_form()
|
||||
self.create_or_edit_team_page.submit_form()
|
||||
|
||||
expected_events = [
|
||||
{
|
||||
'event_type': 'edx.team.changed',
|
||||
'event': {
|
||||
'course_id': self.course_id,
|
||||
'team_id': self.team['id'],
|
||||
'field': 'country',
|
||||
'old': 'AF',
|
||||
'new': 'PK',
|
||||
'truncated': [],
|
||||
}
|
||||
},
|
||||
{
|
||||
'event_type': 'edx.team.changed',
|
||||
'event': {
|
||||
'course_id': self.course_id,
|
||||
'team_id': self.team['id'],
|
||||
'field': 'name',
|
||||
'old': self.team['name'],
|
||||
'new': self.TEAMS_NAME,
|
||||
'truncated': [],
|
||||
}
|
||||
},
|
||||
{
|
||||
'event_type': 'edx.team.changed',
|
||||
'event': {
|
||||
'course_id': self.course_id,
|
||||
'team_id': self.team['id'],
|
||||
'field': 'language',
|
||||
'old': 'aa',
|
||||
'new': 'en',
|
||||
'truncated': [],
|
||||
}
|
||||
},
|
||||
{
|
||||
'event_type': 'edx.team.changed',
|
||||
'event': {
|
||||
'course_id': self.course_id,
|
||||
'team_id': self.team['id'],
|
||||
'field': 'description',
|
||||
'old': self.team['description'],
|
||||
'new': self.TEAM_DESCRIPTION,
|
||||
'truncated': [],
|
||||
}
|
||||
},
|
||||
]
|
||||
with self.assert_events_match_during(event_filter=self.only_team_events, expected_events=expected_events):
|
||||
self.create_or_edit_team_page.submit_form()
|
||||
|
||||
self.team_page.wait_for_page()
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from datetime import datetime
|
||||
from uuid import uuid4
|
||||
import pytz
|
||||
from datetime import datetime
|
||||
from model_utils import FieldTracker
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.contrib.auth.models import User
|
||||
@@ -89,6 +90,11 @@ class CourseTeam(models.Model):
|
||||
users = models.ManyToManyField(User, db_index=True, related_name='teams', through='CourseTeamMembership')
|
||||
team_size = models.IntegerField(default=0, db_index=True) # indexed for ordering
|
||||
|
||||
field_tracker = FieldTracker()
|
||||
|
||||
# Don't emit changed events when these fields change.
|
||||
FIELD_BLACKLIST = ['last_activity_at', 'team_size']
|
||||
|
||||
@classmethod
|
||||
def create(cls, name, course_id, description, topic_id=None, country=None, language=None):
|
||||
"""Create a complete CourseTeam object.
|
||||
|
||||
@@ -743,9 +743,12 @@ class TestDetailTeamAPI(TeamAPITestCase):
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestUpdateTeamAPI(TeamAPITestCase):
|
||||
class TestUpdateTeamAPI(EventTestMixin, TeamAPITestCase):
|
||||
"""Test cases for the team update endpoint."""
|
||||
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
super(TestUpdateTeamAPI, self).setUp('teams.views.tracker')
|
||||
|
||||
@ddt.data(
|
||||
(None, 401),
|
||||
('student_inactive', 401),
|
||||
@@ -757,9 +760,19 @@ class TestUpdateTeamAPI(TeamAPITestCase):
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_access(self, user, status):
|
||||
prev_name = self.solar_team.name
|
||||
team = self.patch_team_detail(self.solar_team.team_id, status, {'name': 'foo'}, user=user)
|
||||
if status == 200:
|
||||
self.assertEquals(team['name'], 'foo')
|
||||
self.assert_event_emitted(
|
||||
'edx.team.changed',
|
||||
team_id=self.solar_team.team_id,
|
||||
course_id=unicode(self.solar_team.course_id),
|
||||
truncated=[],
|
||||
field='name',
|
||||
old=prev_name,
|
||||
new='foo'
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
(None, 401),
|
||||
@@ -787,8 +800,22 @@ class TestUpdateTeamAPI(TeamAPITestCase):
|
||||
@ddt.data(('country', 'US'), ('language', 'en'), ('foo', 'bar'))
|
||||
@ddt.unpack
|
||||
def test_good_requests(self, key, value):
|
||||
if hasattr(self.solar_team, key):
|
||||
prev_value = getattr(self.solar_team, key)
|
||||
|
||||
self.patch_team_detail(self.solar_team.team_id, 200, {key: value}, user='staff')
|
||||
|
||||
if hasattr(self.solar_team, key):
|
||||
self.assert_event_emitted(
|
||||
'edx.team.changed',
|
||||
team_id=self.solar_team.team_id,
|
||||
course_id=unicode(self.solar_team.course_id),
|
||||
truncated=[],
|
||||
field=key,
|
||||
old=prev_value,
|
||||
new=value
|
||||
)
|
||||
|
||||
def test_does_not_exist(self):
|
||||
self.patch_team_detail('no_such_team', 404, user='staff')
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ from rest_framework.authentication import (
|
||||
from rest_framework import status
|
||||
from rest_framework import permissions
|
||||
from django.db.models import Count
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.contrib.auth.models import User
|
||||
from django_countries import countries
|
||||
from django.utils.translation import ugettext as _
|
||||
@@ -40,6 +42,7 @@ from student.models import CourseEnrollment, CourseAccessRole
|
||||
from student.roles import CourseStaffRole
|
||||
from django_comment_client.utils import has_discussion_privileges
|
||||
from teams import is_feature_enabled
|
||||
from util.model_utils import truncate_fields
|
||||
from .models import CourseTeam, CourseTeamMembership
|
||||
from .serializers import (
|
||||
CourseTeamSerializer,
|
||||
@@ -59,6 +62,25 @@ TOPICS_PER_PAGE = 12
|
||||
MAXIMUM_SEARCH_SIZE = 100000
|
||||
|
||||
|
||||
@receiver(post_save, sender=CourseTeam)
|
||||
def team_post_save_callback(sender, instance, **kwargs): # pylint: disable=unused-argument
|
||||
""" Emits signal after the team is saved. """
|
||||
changed_fields = instance.field_tracker.changed()
|
||||
# Don't emit events when we are first creating the team.
|
||||
if not kwargs['created']:
|
||||
for field in changed_fields:
|
||||
if field not in instance.FIELD_BLACKLIST:
|
||||
truncated_fields = truncate_fields(unicode(changed_fields[field]), unicode(getattr(instance, field)))
|
||||
truncated_fields['team_id'] = instance.team_id
|
||||
truncated_fields['course_id'] = unicode(instance.course_id)
|
||||
truncated_fields['field'] = field
|
||||
|
||||
tracker.emit(
|
||||
'edx.team.changed',
|
||||
truncated_fields
|
||||
)
|
||||
|
||||
|
||||
class TeamsDashboardView(View):
|
||||
"""
|
||||
View methods related to the teams dashboard.
|
||||
|
||||
Reference in New Issue
Block a user