diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 28432c9ab0..69326d265c 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -4831,6 +4831,9 @@ class ActivityStreamSerializer(BaseSerializer): username = obj.actor.username, first_name = obj.actor.first_name, last_name = obj.actor.last_name) + elif obj.deleted_actor: + summary_fields['actor'] = obj.deleted_actor.copy() + summary_fields['actor']['id'] = None if obj.setting: summary_fields['setting'] = [obj.setting] return summary_fields diff --git a/awx/main/migrations/0038_v330_add_deleted_activitystream_actor.py b/awx/main/migrations/0038_v330_add_deleted_activitystream_actor.py new file mode 100644 index 0000000000..6f79485f3f --- /dev/null +++ b/awx/main/migrations/0038_v330_add_deleted_activitystream_actor.py @@ -0,0 +1,24 @@ +#d -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-05-21 19:51 +from __future__ import unicode_literals + +import awx.main.fields +import awx.main.models.activity_stream +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0037_v330_remove_legacy_fact_cleanup'), + ] + + operations = [ + migrations.AddField( + model_name='activitystream', + name='deleted_actor', + field=awx.main.fields.JSONField(null=True), + ), + ] diff --git a/awx/main/models/activity_stream.py b/awx/main/models/activity_stream.py index 45d8cbea07..5881008bf2 100644 --- a/awx/main/models/activity_stream.py +++ b/awx/main/models/activity_stream.py @@ -33,6 +33,7 @@ class ActivityStream(models.Model): operation = models.CharField(max_length=13, choices=OPERATION_CHOICES) timestamp = models.DateTimeField(auto_now_add=True) changes = models.TextField(blank=True) + deleted_actor = JSONField(null=True) object_relationship_type = models.TextField(blank=True) object1 = models.TextField() @@ -77,6 +78,18 @@ class ActivityStream(models.Model): return reverse('api:activity_stream_detail', kwargs={'pk': self.pk}, request=request) def save(self, *args, **kwargs): + # Store denormalized actor metadata so that we retain it for accounting + # purposes when the User row is deleted. + if self.actor: + self.deleted_actor = { + 'id': self.actor_id, + 'username': self.actor.username, + 'first_name': self.actor.first_name, + 'last_name': self.actor.last_name, + } + if 'update_fields' in kwargs and 'deleted_actor' not in kwargs['update_fields']: + kwargs['update_fields'].append('deleted_actor') + # For compatibility with Django 1.4.x, attempt to handle any calls to # save that pass update_fields. try: diff --git a/awx/main/tests/functional/models/test_activity_stream.py b/awx/main/tests/functional/models/test_activity_stream.py index f13ce48f20..eff901093e 100644 --- a/awx/main/tests/functional/models/test_activity_stream.py +++ b/awx/main/tests/functional/models/test_activity_stream.py @@ -184,6 +184,32 @@ def test_annon_user_action(): assert not entry.actor +@pytest.mark.django_db +def test_activity_stream_deleted_actor(alice, bob): + alice.first_name = 'Alice' + alice.last_name = 'Doe' + alice.save() + with impersonate(alice): + o = Organization.objects.create(name='test organization') + entry = o.activitystream_set.get(operation='create') + assert entry.actor == alice + + alice.delete() + entry = o.activitystream_set.get(operation='create') + assert entry.actor is None + deleted = entry.deleted_actor + assert deleted['username'] == 'alice' + assert deleted['first_name'] == 'Alice' + assert deleted['last_name'] == 'Doe' + + entry.actor = bob + entry.save(update_fields=['actor']) + deleted = entry.deleted_actor + + entry = ActivityStream.objects.get(id=entry.pk) + assert entry.deleted_actor['username'] == 'bob' + + @pytest.mark.django_db def test_modified_not_allowed_field(somecloud_type): ''' diff --git a/awx/ui/client/src/activity-stream/activitystream.controller.js b/awx/ui/client/src/activity-stream/activitystream.controller.js index 73c8f9c732..febc257786 100644 --- a/awx/ui/client/src/activity-stream/activitystream.controller.js +++ b/awx/ui/client/src/activity-stream/activitystream.controller.js @@ -53,8 +53,12 @@ export default ['$scope', '$state', 'subTitle', 'GetTargetTitle', $scope.activities.forEach(function(activity, i) { // build activity.user if ($scope.activities[i].summary_fields.actor) { - $scope.activities[i].user = "" + - $scope.activities[i].summary_fields.actor.username + ""; + if ($scope.activities[i].summary_fields.actor.id) { + $scope.activities[i].user = "" + + $scope.activities[i].summary_fields.actor.username + ""; + } else { + $scope.activities[i].user = $scope.activities[i].summary_fields.actor.username + ' (deleted)'; + } } else { $scope.activities[i].user = 'system'; }