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';
}