From 25704af172ce7b4cf8af4a4f45ab3b1213f42b80 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 8 Nov 2013 04:39:53 -0500 Subject: [PATCH 01/11] Initial backend implementation for AC-25, activity stream/audit love --- awx/api/urls.py | 6 ++++ awx/main/middleware.py | 24 ++++++++++++++++ awx/main/models/__init__.py | 43 ++++++++++++++++++++++++++++ awx/main/registrar.py | 34 ++++++++++++++++++++++ awx/main/signals.py | 57 +++++++++++++++++++++++++++++++++++++ awx/main/utils.py | 36 +++++++++++++++++++++++ awx/settings/defaults.py | 1 + 7 files changed, 201 insertions(+) create mode 100644 awx/main/middleware.py create mode 100644 awx/main/registrar.py diff --git a/awx/api/urls.py b/awx/api/urls.py index e868610231..47a470aaa2 100644 --- a/awx/api/urls.py +++ b/awx/api/urls.py @@ -141,6 +141,11 @@ job_event_urls = patterns('awx.api.views', url(r'^(?P[0-9]+)/hosts/$', 'job_event_hosts_list'), ) +# activity_stream_urls = patterns('awx.api.views', +# url(r'^$', 'activity_stream_list'), +# url(r'^(?P[0-9]+)/$', 'activity_stream_detail'), +# ) + v1_urls = patterns('awx.api.views', url(r'^$', 'api_v1_root_view'), url(r'^config/$', 'api_v1_config_view'), @@ -162,6 +167,7 @@ v1_urls = patterns('awx.api.views', url(r'^jobs/', include(job_urls)), url(r'^job_host_summaries/', include(job_host_summary_urls)), url(r'^job_events/', include(job_event_urls)), + # url(r'^activity_stream/', include(activity_stream_urls)), ) urlpatterns = patterns('awx.api.views', diff --git a/awx/main/middleware.py b/awx/main/middleware.py new file mode 100644 index 0000000000..33a1e51f50 --- /dev/null +++ b/awx/main/middleware.py @@ -0,0 +1,24 @@ +from django.conf import settings +from django.db.models.signals import pre_save +from django.utils.functional import curry +from awx.main.models import ActivityStream + + +class ActvitiyStreamMiddleware(object): + + def process_request(self, request): + if hasattr(request, 'user') and hasattr(request.user, 'is_authenticated') and request.user.is_authenticated(): + user = request.user + else: + user = None + + set_actor = curry(self.set_actor, user) + pre_save.connect(set_actor, sender=ActivityStream, dispatch_uid=(self.__class__, request), weak=False) + + def process_response(self, request, response): + pre_save.disconnect(dispatch_uid=(self.__class__, request)) + return response + + def set_actor(self, user, sender, instance, **kwargs): + if sender == ActivityStream and isinstance(user, settings.AUTH_USER_MODEL) and instance.user is None: + instance.user = user diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index d76258d926..9c20dee97d 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -38,6 +38,7 @@ from djcelery.models import TaskMeta from awx.lib.compat import slugify from awx.main.fields import AutoOneToOneField from awx.main.utils import encrypt_field, decrypt_field +from awx.main.registrar import activity_stream_registrar __all__ = ['PrimordialModel', 'Organization', 'Team', 'Project', 'ProjectUpdate', 'Credential', 'Inventory', 'Host', 'Group', @@ -2242,6 +2243,31 @@ class AuthToken(models.Model): def __unicode__(self): return self.key +class ActivityStream(models.Model): + ''' + Model used to describe activity stream (audit) events + ''' + OPERATION_CHOICES = [ + ('create', _('Entity Created')), + ('update', _("Entity Updated")), + ('delete', _("Entity Deleted")), + ('associate', _("Entity Associated with another Entity")), + ('disaassociate', _("Entity was Disassociated with another Entity")) + ] + + user = models.ForeignKey('auth.User', null=True, on_delete=SET_NULL) + operation = models.CharField(max_length=9, choices=OPERATION_CHOICES) + timestamp = models.DateTimeField(auto_now_add=True) + changes = models.TextField(blank=True) + + object1_id = models.PositiveIntegerField(db_index=True) + object1_type = models.TextField() + + object2_id = models.PositiveIntegerField(db_index=True) + object2_type = models.TextField() + + object_relationship_type = models.TextField() + # TODO: reporting (MPD) # Add mark_inactive method to User model. @@ -2276,3 +2302,20 @@ _PythonSerializer.handle_m2m_field = _new_handle_m2m_field # Import signal handlers only after models have been defined. import awx.main.signals + +activity_stream_registrar.connect(Organization) +activity_stream_registrar.connect(Inventory) +activity_stream_registrar.connect(Host) +activity_stream_registrar.connect(Group) +activity_stream_registrar.connect(InventorySource) +activity_stream_registrar.connect(InventoryUpdate) +activity_stream_registrar.connect(Credential) +activity_stream_registrar.connect(Team) +activity_stream_registrar.connect(Project) +activity_stream_registrar.connect(ProjectUpdate) +activity_stream_registrar.connect(Permission) +activity_stream_registrar.connect(JobTemplate) +activity_stream_registrar.connect(Job) +activity_stream_registrar.connect(JobHostSummary) +activity_stream_registrar.connect(JobEvent) +activity_stream_registrar.connect(Profile) diff --git a/awx/main/registrar.py b/awx/main/registrar.py new file mode 100644 index 0000000000..f5c631a053 --- /dev/null +++ b/awx/main/registrar.py @@ -0,0 +1,34 @@ +from django.db.models.signals import pre_save, post_save, post_delete, m2m_changed +from signals import activity_stream_create, activity_stream_update, activity_stream_delete + +class ActivityStreamRegistrar(object): + + def __init__(self): + self.models = [] + + def connect(self, model): + #(receiver, sender=model, dispatch_uid=self._dispatch_uid(signal, model)) + if model not in self.models: + self.models.append(model) + post_save.connect(activity_stream_create, sender=model, dispatch_uid=self.__class__ + str(model) + "_create") + pre_save.connect(activity_stream_update, sender=model, dispatch_uid=self.__class__ + str(model) + "_update") + post_delete.connect(activity_stream_delete, sender=model, dispatch_uid=self.__class__ + str(model) + "_delete") + + for m2mfield in model._meta.many_to_many: + m2m_attr = get_attr(model, m2mfield.name) + m2m_changed.connect(activity_stream_associate, sender=m2m_attr.through, + dispatch_uid=self.__class__ + str(m2m_attr.through) + "_associate") + + def disconnect(self, model): + if model in self.models: + post_save.disconnect(dispatch_uid=self.__class__ + str(model) + "_create") + pre_save.disconnect(dispatch_uid=self.__class__ + str(model) + "_update") + post_delete.disconnect(dispatch_uid=self.__class__ + str(model) + "_delete") + self.models.pop(model) + + + for m2mfield in model._meta.many_to_many: + m2m_attr = get_attr(model, m2mfield.name) + m2m_changed.disconnect(dispatch_uid=self.__class__ + str(m2m_attr.through) + "_associate") + +activity_stream_registrar = ActivityStreamRegistrar() diff --git a/awx/main/signals.py b/awx/main/signals.py index 9bcf1bb9f3..1b77338cd9 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -11,6 +11,7 @@ from django.dispatch import receiver # AWX from awx.main.models import * +from awx.main.utils import model_instance_diff __all__ = [] @@ -168,3 +169,59 @@ def update_host_last_job_after_job_deleted(sender, **kwargs): hosts_pks = getattr(instance, '_saved_hosts_pks', []) for host in Host.objects.filter(pk__in=hosts_pks): _update_host_last_jhs(host) + +# Set via ActivityStreamRegistrar to record activity stream events + +def activity_stream_create(sender, instance, created, **kwargs): + if created: + activity_entry = ActivityStream( + operation='create', + object1_id=instance.id, + object1_type=instance.__class__) + activity_entry.save() + +def activity_stream_update(sender, instance, **kwargs): + try: + old = sender.objects.get(id=instance.id) + except sender.DoesNotExist: + pass + + new = instance + changes = model_instance_diff(old, new) + activity_entry = ActivityStream( + operation='update', + object1_id=instance.id, + object1_type=instance.__class__, + changes=json.dumps(changes)) + activity_entry.save() + + +def activity_stream_delete(sender, instance, **kwargs): + activity_entry = ActivityStream( + operation='delete', + object1_id=instance.id, + object1_type=instance.__class__) + activity_entry.save() + +def activity_stream_associate(sender, instance, **kwargs): + if 'pre_add' in kwargs['action'] or 'pre_remove' in kwargs['action']: + if kwargs['action'] == 'pre_add': + action = 'associate' + elif kwargs['action'] == 'pre_remove': + action = 'disassociate' + else: + return + obj1 = instance + obj1_id = obj1.id + obj_rel = str(sender) + for entity_acted in kwargs['pk_set']: + obj2 = entity_acted + obj2_id = entity_acted.id + activity_entry = ActivityStream( + operation=action, + object1_id=obj1_id, + object1_type=obj1.__class__, + object2_id=obj2_id, + object2_type=obj2.__class__, + object_relationship_type=obj_rel) + activity_entry.save() diff --git a/awx/main/utils.py b/awx/main/utils.py index 562884a346..3fdfa21aa2 100644 --- a/awx/main/utils.py +++ b/awx/main/utils.py @@ -10,6 +10,9 @@ import subprocess import sys import urlparse +# Django +from django.db.models import Model + # Django REST Framework from rest_framework.exceptions import ParseError, PermissionDenied @@ -219,3 +222,36 @@ def update_scm_url(scm_type, url, username=True, password=True): new_url = urlparse.urlunsplit([parts.scheme, netloc, parts.path, parts.query, parts.fragment]) return new_url + +def model_instance_diff(old, new): + """ + Calculate the differences between two model instances. One of the instances may be None (i.e., a newly + created model or deleted model). This will cause all fields with a value to have changed (from None). + """ + if not(old is None or isinstance(old, Model)): + raise TypeError('The supplied old instance is not a valid model instance.') + if not(new is None or isinstance(new, Model)): + raise TypeError('The supplied new instance is not a valid model instance.') + + diff = {} + + if old is not None and new is not None: + fields = set(old._meta.fields + new._meta.fields) + elif old is not None: + fields = set(old._meta.fields) + elif new is not None: + fields = set(new._meta.fields) + else: + fields = set() + + for field in fields: + old_value = str(getattr(old, field.name, None)) + new_value = str(getattr(new, field.name, None)) + + if old_value != new_value: + diff[field.name] = (old_value, new_value) + + if len(diff) == 0: + diff = None + + return diff diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index c9bdbe73e1..b2cdc67dee 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -105,6 +105,7 @@ TEMPLATE_CONTEXT_PROCESSORS += ( MIDDLEWARE_CLASSES += ( 'django.middleware.transaction.TransactionMiddleware', # Middleware loaded after this point will be subject to transactions. + 'awx.main.middleware.ActivityStreamMiddleware' ) TEMPLATE_DIRS = ( From cdccfee93a299ddb1c3f639df822cc1eea2617a6 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 8 Nov 2013 04:39:53 -0500 Subject: [PATCH 02/11] Initial backend implementation for AC-25, activity stream/audit love --- awx/api/urls.py | 6 ++++ awx/main/middleware.py | 24 ++++++++++++++++ awx/main/models/__init__.py | 17 +++++++++++ awx/main/models/base.py | 25 ++++++++++++++++ awx/main/registrar.py | 34 ++++++++++++++++++++++ awx/main/signals.py | 57 +++++++++++++++++++++++++++++++++++++ awx/main/utils.py | 36 +++++++++++++++++++++++ awx/settings/defaults.py | 1 + 8 files changed, 200 insertions(+) create mode 100644 awx/main/middleware.py create mode 100644 awx/main/registrar.py diff --git a/awx/api/urls.py b/awx/api/urls.py index e868610231..47a470aaa2 100644 --- a/awx/api/urls.py +++ b/awx/api/urls.py @@ -141,6 +141,11 @@ job_event_urls = patterns('awx.api.views', url(r'^(?P[0-9]+)/hosts/$', 'job_event_hosts_list'), ) +# activity_stream_urls = patterns('awx.api.views', +# url(r'^$', 'activity_stream_list'), +# url(r'^(?P[0-9]+)/$', 'activity_stream_detail'), +# ) + v1_urls = patterns('awx.api.views', url(r'^$', 'api_v1_root_view'), url(r'^config/$', 'api_v1_config_view'), @@ -162,6 +167,7 @@ v1_urls = patterns('awx.api.views', url(r'^jobs/', include(job_urls)), url(r'^job_host_summaries/', include(job_host_summary_urls)), url(r'^job_events/', include(job_event_urls)), + # url(r'^activity_stream/', include(activity_stream_urls)), ) urlpatterns = patterns('awx.api.views', diff --git a/awx/main/middleware.py b/awx/main/middleware.py new file mode 100644 index 0000000000..33a1e51f50 --- /dev/null +++ b/awx/main/middleware.py @@ -0,0 +1,24 @@ +from django.conf import settings +from django.db.models.signals import pre_save +from django.utils.functional import curry +from awx.main.models import ActivityStream + + +class ActvitiyStreamMiddleware(object): + + def process_request(self, request): + if hasattr(request, 'user') and hasattr(request.user, 'is_authenticated') and request.user.is_authenticated(): + user = request.user + else: + user = None + + set_actor = curry(self.set_actor, user) + pre_save.connect(set_actor, sender=ActivityStream, dispatch_uid=(self.__class__, request), weak=False) + + def process_response(self, request, response): + pre_save.disconnect(dispatch_uid=(self.__class__, request)) + return response + + def set_actor(self, user, sender, instance, **kwargs): + if sender == ActivityStream and isinstance(user, settings.AUTH_USER_MODEL) and instance.user is None: + instance.user = user diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index abb68b57da..e25752e403 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -27,3 +27,20 @@ User.add_to_class('can_access', check_user_access) # Import signal handlers only after models have been defined. import awx.main.signals + +activity_stream_registrar.connect(Organization) +activity_stream_registrar.connect(Inventory) +activity_stream_registrar.connect(Host) +activity_stream_registrar.connect(Group) +activity_stream_registrar.connect(InventorySource) +activity_stream_registrar.connect(InventoryUpdate) +activity_stream_registrar.connect(Credential) +activity_stream_registrar.connect(Team) +activity_stream_registrar.connect(Project) +activity_stream_registrar.connect(ProjectUpdate) +activity_stream_registrar.connect(Permission) +activity_stream_registrar.connect(JobTemplate) +activity_stream_registrar.connect(Job) +activity_stream_registrar.connect(JobHostSummary) +activity_stream_registrar.connect(JobEvent) +activity_stream_registrar.connect(Profile) diff --git a/awx/main/models/base.py b/awx/main/models/base.py index 776f500972..bddcb4be1a 100644 --- a/awx/main/models/base.py +++ b/awx/main/models/base.py @@ -333,3 +333,28 @@ class CommonTask(PrimordialModel): self.cancel_flag = True self.save(update_fields=['cancel_flag']) return self.cancel_flag + +class ActivityStream(models.Model): + ''' + Model used to describe activity stream (audit) events + ''' + OPERATION_CHOICES = [ + ('create', _('Entity Created')), + ('update', _("Entity Updated")), + ('delete', _("Entity Deleted")), + ('associate', _("Entity Associated with another Entity")), + ('disaassociate', _("Entity was Disassociated with another Entity")) + ] + + user = models.ForeignKey('auth.User', null=True, on_delete=SET_NULL) + operation = models.CharField(max_length=9, choices=OPERATION_CHOICES) + timestamp = models.DateTimeField(auto_now_add=True) + changes = models.TextField(blank=True) + + object1_id = models.PositiveIntegerField(db_index=True) + object1_type = models.TextField() + + object2_id = models.PositiveIntegerField(db_index=True) + object2_type = models.TextField() + + object_relationship_type = models.TextField() diff --git a/awx/main/registrar.py b/awx/main/registrar.py new file mode 100644 index 0000000000..f5c631a053 --- /dev/null +++ b/awx/main/registrar.py @@ -0,0 +1,34 @@ +from django.db.models.signals import pre_save, post_save, post_delete, m2m_changed +from signals import activity_stream_create, activity_stream_update, activity_stream_delete + +class ActivityStreamRegistrar(object): + + def __init__(self): + self.models = [] + + def connect(self, model): + #(receiver, sender=model, dispatch_uid=self._dispatch_uid(signal, model)) + if model not in self.models: + self.models.append(model) + post_save.connect(activity_stream_create, sender=model, dispatch_uid=self.__class__ + str(model) + "_create") + pre_save.connect(activity_stream_update, sender=model, dispatch_uid=self.__class__ + str(model) + "_update") + post_delete.connect(activity_stream_delete, sender=model, dispatch_uid=self.__class__ + str(model) + "_delete") + + for m2mfield in model._meta.many_to_many: + m2m_attr = get_attr(model, m2mfield.name) + m2m_changed.connect(activity_stream_associate, sender=m2m_attr.through, + dispatch_uid=self.__class__ + str(m2m_attr.through) + "_associate") + + def disconnect(self, model): + if model in self.models: + post_save.disconnect(dispatch_uid=self.__class__ + str(model) + "_create") + pre_save.disconnect(dispatch_uid=self.__class__ + str(model) + "_update") + post_delete.disconnect(dispatch_uid=self.__class__ + str(model) + "_delete") + self.models.pop(model) + + + for m2mfield in model._meta.many_to_many: + m2m_attr = get_attr(model, m2mfield.name) + m2m_changed.disconnect(dispatch_uid=self.__class__ + str(m2m_attr.through) + "_associate") + +activity_stream_registrar = ActivityStreamRegistrar() diff --git a/awx/main/signals.py b/awx/main/signals.py index 9bcf1bb9f3..1b77338cd9 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -11,6 +11,7 @@ from django.dispatch import receiver # AWX from awx.main.models import * +from awx.main.utils import model_instance_diff __all__ = [] @@ -168,3 +169,59 @@ def update_host_last_job_after_job_deleted(sender, **kwargs): hosts_pks = getattr(instance, '_saved_hosts_pks', []) for host in Host.objects.filter(pk__in=hosts_pks): _update_host_last_jhs(host) + +# Set via ActivityStreamRegistrar to record activity stream events + +def activity_stream_create(sender, instance, created, **kwargs): + if created: + activity_entry = ActivityStream( + operation='create', + object1_id=instance.id, + object1_type=instance.__class__) + activity_entry.save() + +def activity_stream_update(sender, instance, **kwargs): + try: + old = sender.objects.get(id=instance.id) + except sender.DoesNotExist: + pass + + new = instance + changes = model_instance_diff(old, new) + activity_entry = ActivityStream( + operation='update', + object1_id=instance.id, + object1_type=instance.__class__, + changes=json.dumps(changes)) + activity_entry.save() + + +def activity_stream_delete(sender, instance, **kwargs): + activity_entry = ActivityStream( + operation='delete', + object1_id=instance.id, + object1_type=instance.__class__) + activity_entry.save() + +def activity_stream_associate(sender, instance, **kwargs): + if 'pre_add' in kwargs['action'] or 'pre_remove' in kwargs['action']: + if kwargs['action'] == 'pre_add': + action = 'associate' + elif kwargs['action'] == 'pre_remove': + action = 'disassociate' + else: + return + obj1 = instance + obj1_id = obj1.id + obj_rel = str(sender) + for entity_acted in kwargs['pk_set']: + obj2 = entity_acted + obj2_id = entity_acted.id + activity_entry = ActivityStream( + operation=action, + object1_id=obj1_id, + object1_type=obj1.__class__, + object2_id=obj2_id, + object2_type=obj2.__class__, + object_relationship_type=obj_rel) + activity_entry.save() diff --git a/awx/main/utils.py b/awx/main/utils.py index 562884a346..3fdfa21aa2 100644 --- a/awx/main/utils.py +++ b/awx/main/utils.py @@ -10,6 +10,9 @@ import subprocess import sys import urlparse +# Django +from django.db.models import Model + # Django REST Framework from rest_framework.exceptions import ParseError, PermissionDenied @@ -219,3 +222,36 @@ def update_scm_url(scm_type, url, username=True, password=True): new_url = urlparse.urlunsplit([parts.scheme, netloc, parts.path, parts.query, parts.fragment]) return new_url + +def model_instance_diff(old, new): + """ + Calculate the differences between two model instances. One of the instances may be None (i.e., a newly + created model or deleted model). This will cause all fields with a value to have changed (from None). + """ + if not(old is None or isinstance(old, Model)): + raise TypeError('The supplied old instance is not a valid model instance.') + if not(new is None or isinstance(new, Model)): + raise TypeError('The supplied new instance is not a valid model instance.') + + diff = {} + + if old is not None and new is not None: + fields = set(old._meta.fields + new._meta.fields) + elif old is not None: + fields = set(old._meta.fields) + elif new is not None: + fields = set(new._meta.fields) + else: + fields = set() + + for field in fields: + old_value = str(getattr(old, field.name, None)) + new_value = str(getattr(new, field.name, None)) + + if old_value != new_value: + diff[field.name] = (old_value, new_value) + + if len(diff) == 0: + diff = None + + return diff diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index dac6a86714..80f8650411 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -105,6 +105,7 @@ TEMPLATE_CONTEXT_PROCESSORS += ( MIDDLEWARE_CLASSES += ( 'django.middleware.transaction.TransactionMiddleware', # Middleware loaded after this point will be subject to transactions. + 'awx.main.middleware.ActivityStreamMiddleware' ) TEMPLATE_DIRS = ( From be28a298eb3bf6e8f48128f3ccb7d903dd7da948 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 8 Nov 2013 10:04:12 -0500 Subject: [PATCH 03/11] Fix some inconsistencies after the merge --- awx/main/models/__init__.py | 1 + awx/main/models/base.py | 2 +- awx/main/registrar.py | 22 +++++++++++----------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index e25752e403..05d38c027b 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -6,6 +6,7 @@ from awx.main.models.organization import * from awx.main.models.projects import * from awx.main.models.inventory import * from awx.main.models.jobs import * +from awx.main.registrar import activity_stream_registrar # Monkeypatch Django serializer to ignore django-taggit fields (which break # the dumpdata command; see https://github.com/alex/django-taggit/issues/155). diff --git a/awx/main/models/base.py b/awx/main/models/base.py index bddcb4be1a..53fc46326d 100644 --- a/awx/main/models/base.py +++ b/awx/main/models/base.py @@ -346,7 +346,7 @@ class ActivityStream(models.Model): ('disaassociate', _("Entity was Disassociated with another Entity")) ] - user = models.ForeignKey('auth.User', null=True, on_delete=SET_NULL) + user = models.ForeignKey('auth.User', null=True, on_delete=models.SET_NULL) operation = models.CharField(max_length=9, choices=OPERATION_CHOICES) timestamp = models.DateTimeField(auto_now_add=True) changes = models.TextField(blank=True) diff --git a/awx/main/registrar.py b/awx/main/registrar.py index f5c631a053..f4a6b64484 100644 --- a/awx/main/registrar.py +++ b/awx/main/registrar.py @@ -1,5 +1,5 @@ from django.db.models.signals import pre_save, post_save, post_delete, m2m_changed -from signals import activity_stream_create, activity_stream_update, activity_stream_delete +from signals import activity_stream_create, activity_stream_update, activity_stream_delete, activity_stream_associate class ActivityStreamRegistrar(object): @@ -10,25 +10,25 @@ class ActivityStreamRegistrar(object): #(receiver, sender=model, dispatch_uid=self._dispatch_uid(signal, model)) if model not in self.models: self.models.append(model) - post_save.connect(activity_stream_create, sender=model, dispatch_uid=self.__class__ + str(model) + "_create") - pre_save.connect(activity_stream_update, sender=model, dispatch_uid=self.__class__ + str(model) + "_update") - post_delete.connect(activity_stream_delete, sender=model, dispatch_uid=self.__class__ + str(model) + "_delete") + post_save.connect(activity_stream_create, sender=model, dispatch_uid=str(self.__class__) + str(model) + "_create") + pre_save.connect(activity_stream_update, sender=model, dispatch_uid=str(self.__class__) + str(model) + "_update") + post_delete.connect(activity_stream_delete, sender=model, dispatch_uid=str(self.__class__) + str(model) + "_delete") for m2mfield in model._meta.many_to_many: - m2m_attr = get_attr(model, m2mfield.name) + m2m_attr = getattr(model, m2mfield.name) m2m_changed.connect(activity_stream_associate, sender=m2m_attr.through, - dispatch_uid=self.__class__ + str(m2m_attr.through) + "_associate") + dispatch_uid=str(self.__class__) + str(m2m_attr.through) + "_associate") def disconnect(self, model): if model in self.models: - post_save.disconnect(dispatch_uid=self.__class__ + str(model) + "_create") - pre_save.disconnect(dispatch_uid=self.__class__ + str(model) + "_update") - post_delete.disconnect(dispatch_uid=self.__class__ + str(model) + "_delete") + post_save.disconnect(dispatch_uid=str(self.__class__) + str(model) + "_create") + pre_save.disconnect(dispatch_uid=str(self.__class__) + str(model) + "_update") + post_delete.disconnect(dispatch_uid=str(self.__class__) + str(model) + "_delete") self.models.pop(model) for m2mfield in model._meta.many_to_many: - m2m_attr = get_attr(model, m2mfield.name) - m2m_changed.disconnect(dispatch_uid=self.__class__ + str(m2m_attr.through) + "_associate") + m2m_attr = getattr(model, m2mfield.name) + m2m_changed.disconnect(dispatch_uid=str(self.__class__) + str(m2m_attr.through) + "_associate") activity_stream_registrar = ActivityStreamRegistrar() From f3d2b0b5bce875677e707bc6bd1344b69a9d708d Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 8 Nov 2013 10:21:30 -0500 Subject: [PATCH 04/11] Catch some errors that pop up during ActivityStream testing --- awx/main/middleware.py | 4 ++-- awx/main/models/__init__.py | 2 +- awx/main/registrar.py | 13 ++++++++++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/awx/main/middleware.py b/awx/main/middleware.py index 33a1e51f50..04aacc239d 100644 --- a/awx/main/middleware.py +++ b/awx/main/middleware.py @@ -1,10 +1,10 @@ from django.conf import settings from django.db.models.signals import pre_save from django.utils.functional import curry -from awx.main.models import ActivityStream +from awx.main.models.base import ActivityStream -class ActvitiyStreamMiddleware(object): +class ActivityStreamMiddleware(object): def process_request(self, request): if hasattr(request, 'user') and hasattr(request.user, 'is_authenticated') and request.user.is_authenticated(): diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index 05d38c027b..150741c440 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -44,4 +44,4 @@ activity_stream_registrar.connect(JobTemplate) activity_stream_registrar.connect(Job) activity_stream_registrar.connect(JobHostSummary) activity_stream_registrar.connect(JobEvent) -activity_stream_registrar.connect(Profile) +#activity_stream_registrar.connect(Profile) diff --git a/awx/main/registrar.py b/awx/main/registrar.py index f4a6b64484..863e707423 100644 --- a/awx/main/registrar.py +++ b/awx/main/registrar.py @@ -1,6 +1,10 @@ +import logging + from django.db.models.signals import pre_save, post_save, post_delete, m2m_changed from signals import activity_stream_create, activity_stream_update, activity_stream_delete, activity_stream_associate +logger = logging.getLogger('awx.main.registrar') + class ActivityStreamRegistrar(object): def __init__(self): @@ -15,9 +19,12 @@ class ActivityStreamRegistrar(object): post_delete.connect(activity_stream_delete, sender=model, dispatch_uid=str(self.__class__) + str(model) + "_delete") for m2mfield in model._meta.many_to_many: - m2m_attr = getattr(model, m2mfield.name) - m2m_changed.connect(activity_stream_associate, sender=m2m_attr.through, - dispatch_uid=str(self.__class__) + str(m2m_attr.through) + "_associate") + try: + m2m_attr = getattr(model, m2mfield.name) + m2m_changed.connect(activity_stream_associate, sender=m2m_attr.through, + dispatch_uid=str(self.__class__) + str(m2m_attr.through) + "_associate") + except AttributeError: + logger.warning("Failed to attach m2m activity stream tracker on class %s attribute %s" % (model, m2mfield.name)) def disconnect(self, model): if model in self.models: From 5986a480363ab956d22840e48a89ee8233ada030 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 8 Nov 2013 11:08:11 -0500 Subject: [PATCH 05/11] Fix an issue with the Activity Stream where we weren't performing the right type conversion to track object types --- awx/main/middleware.py | 3 ++- awx/main/models/base.py | 2 +- awx/main/registrar.py | 3 ++- awx/main/signals.py | 12 ++++++------ 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/awx/main/middleware.py b/awx/main/middleware.py index 04aacc239d..3ac7e059fd 100644 --- a/awx/main/middleware.py +++ b/awx/main/middleware.py @@ -1,4 +1,5 @@ from django.conf import settings +from django.contrib.auth.models import User from django.db.models.signals import pre_save from django.utils.functional import curry from awx.main.models.base import ActivityStream @@ -20,5 +21,5 @@ class ActivityStreamMiddleware(object): return response def set_actor(self, user, sender, instance, **kwargs): - if sender == ActivityStream and isinstance(user, settings.AUTH_USER_MODEL) and instance.user is None: + if sender == ActivityStream and isinstance(user, User) and instance.user is None: instance.user = user diff --git a/awx/main/models/base.py b/awx/main/models/base.py index 53fc46326d..99c530004d 100644 --- a/awx/main/models/base.py +++ b/awx/main/models/base.py @@ -22,7 +22,7 @@ from taggit.managers import TaggableManager # Django-Celery from djcelery.models import TaskMeta -__all__ = ['VarsDictProperty', 'PrimordialModel', 'CommonModel', +__all__ = ['VarsDictProperty', 'PrimordialModel', 'CommonModel', 'ActivityStream', 'CommonModelNameNotUnique', 'CommonTask', 'PERM_INVENTORY_ADMIN', 'PERM_INVENTORY_READ', 'PERM_INVENTORY_WRITE', 'PERM_INVENTORY_DEPLOY', 'PERM_INVENTORY_CHECK', 'JOB_TYPE_CHOICES', diff --git a/awx/main/registrar.py b/awx/main/registrar.py index 863e707423..c339c17116 100644 --- a/awx/main/registrar.py +++ b/awx/main/registrar.py @@ -24,7 +24,8 @@ class ActivityStreamRegistrar(object): m2m_changed.connect(activity_stream_associate, sender=m2m_attr.through, dispatch_uid=str(self.__class__) + str(m2m_attr.through) + "_associate") except AttributeError: - logger.warning("Failed to attach m2m activity stream tracker on class %s attribute %s" % (model, m2mfield.name)) + pass + #logger.warning("Failed to attach m2m activity stream tracker on class %s attribute %s" % (model, m2mfield.name)) def disconnect(self, model): if model in self.models: diff --git a/awx/main/signals.py b/awx/main/signals.py index 1b77338cd9..06abd69d81 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -177,21 +177,21 @@ def activity_stream_create(sender, instance, created, **kwargs): activity_entry = ActivityStream( operation='create', object1_id=instance.id, - object1_type=instance.__class__) + object1_type=str(instance.__class__)) activity_entry.save() def activity_stream_update(sender, instance, **kwargs): try: old = sender.objects.get(id=instance.id) except sender.DoesNotExist: - pass + return new = instance changes = model_instance_diff(old, new) activity_entry = ActivityStream( operation='update', object1_id=instance.id, - object1_type=instance.__class__, + object1_type=str(instance.__class__), changes=json.dumps(changes)) activity_entry.save() @@ -200,7 +200,7 @@ def activity_stream_delete(sender, instance, **kwargs): activity_entry = ActivityStream( operation='delete', object1_id=instance.id, - object1_type=instance.__class__) + object1_type=str(instance.__class__)) activity_entry.save() def activity_stream_associate(sender, instance, **kwargs): @@ -220,8 +220,8 @@ def activity_stream_associate(sender, instance, **kwargs): activity_entry = ActivityStream( operation=action, object1_id=obj1_id, - object1_type=obj1.__class__, + object1_type=str(obj1.__class__), object2_id=obj2_id, - object2_type=obj2.__class__, + object2_type=str(obj2.__class__), object_relationship_type=obj_rel) activity_entry.save() From 598925047176729e11fc7fb2cf21b0710a176fa4 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 8 Nov 2013 11:16:04 -0500 Subject: [PATCH 06/11] Move ActivityStream to it's own model file --- awx/main/middleware.py | 2 +- awx/main/models/__init__.py | 1 + awx/main/models/activity_stream.py | 30 ++++++++++++++++++++++++++++++ awx/main/models/base.py | 2 +- 4 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 awx/main/models/activity_stream.py diff --git a/awx/main/middleware.py b/awx/main/middleware.py index 3ac7e059fd..2426d58790 100644 --- a/awx/main/middleware.py +++ b/awx/main/middleware.py @@ -2,7 +2,7 @@ from django.conf import settings from django.contrib.auth.models import User from django.db.models.signals import pre_save from django.utils.functional import curry -from awx.main.models.base import ActivityStream +from awx.main.models.activity_stream import ActivityStream class ActivityStreamMiddleware(object): diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index 150741c440..63cc7ddafa 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -6,6 +6,7 @@ from awx.main.models.organization import * from awx.main.models.projects import * from awx.main.models.inventory import * from awx.main.models.jobs import * +from awx.main.models.activity_stream import * from awx.main.registrar import activity_stream_registrar # Monkeypatch Django serializer to ignore django-taggit fields (which break diff --git a/awx/main/models/activity_stream.py b/awx/main/models/activity_stream.py new file mode 100644 index 0000000000..57cbff7b73 --- /dev/null +++ b/awx/main/models/activity_stream.py @@ -0,0 +1,30 @@ +# Copyright (c) 2013 AnsibleWorks, Inc. +# All Rights Reserved. + + +from django.db import models + +class ActivityStream(models.Model): + ''' + Model used to describe activity stream (audit) events + ''' + OPERATION_CHOICES = [ + ('create', _('Entity Created')), + ('update', _("Entity Updated")), + ('delete', _("Entity Deleted")), + ('associate', _("Entity Associated with another Entity")), + ('disaassociate', _("Entity was Disassociated with another Entity")) + ] + + user = models.ForeignKey('auth.User', null=True, on_delete=models.SET_NULL) + operation = models.CharField(max_length=9, choices=OPERATION_CHOICES) + timestamp = models.DateTimeField(auto_now_add=True) + changes = models.TextField(blank=True) + + object1_id = models.PositiveIntegerField(db_index=True) + object1_type = models.TextField() + + object2_id = models.PositiveIntegerField(db_index=True) + object2_type = models.TextField() + + object_relationship_type = models.TextField() diff --git a/awx/main/models/base.py b/awx/main/models/base.py index 99c530004d..53fc46326d 100644 --- a/awx/main/models/base.py +++ b/awx/main/models/base.py @@ -22,7 +22,7 @@ from taggit.managers import TaggableManager # Django-Celery from djcelery.models import TaskMeta -__all__ = ['VarsDictProperty', 'PrimordialModel', 'CommonModel', 'ActivityStream', +__all__ = ['VarsDictProperty', 'PrimordialModel', 'CommonModel', 'CommonModelNameNotUnique', 'CommonTask', 'PERM_INVENTORY_ADMIN', 'PERM_INVENTORY_READ', 'PERM_INVENTORY_WRITE', 'PERM_INVENTORY_DEPLOY', 'PERM_INVENTORY_CHECK', 'JOB_TYPE_CHOICES', From f4e36132448a4a55bff5660b3f5a669e0095ecc5 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 8 Nov 2013 11:36:36 -0500 Subject: [PATCH 07/11] Fix up some issues with supporting schema migration --- awx/main/models/activity_stream.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/awx/main/models/activity_stream.py b/awx/main/models/activity_stream.py index 57cbff7b73..5d4440a7e7 100644 --- a/awx/main/models/activity_stream.py +++ b/awx/main/models/activity_stream.py @@ -3,11 +3,16 @@ from django.db import models +from django.utils.translation import ugettext_lazy as _ class ActivityStream(models.Model): ''' Model used to describe activity stream (audit) events ''' + + class Meta: + app_label = 'main' + OPERATION_CHOICES = [ ('create', _('Entity Created')), ('update', _("Entity Updated")), @@ -16,7 +21,7 @@ class ActivityStream(models.Model): ('disaassociate', _("Entity was Disassociated with another Entity")) ] - user = models.ForeignKey('auth.User', null=True, on_delete=models.SET_NULL) + user = models.ForeignKey('auth.User', null=True, on_delete=models.SET_NULL, related_name='activity_stream') operation = models.CharField(max_length=9, choices=OPERATION_CHOICES) timestamp = models.DateTimeField(auto_now_add=True) changes = models.TextField(blank=True) @@ -24,7 +29,7 @@ class ActivityStream(models.Model): object1_id = models.PositiveIntegerField(db_index=True) object1_type = models.TextField() - object2_id = models.PositiveIntegerField(db_index=True) - object2_type = models.TextField() + object2_id = models.PositiveIntegerField(db_index=True, null=True) + object2_type = models.TextField(null=True, blank=True) - object_relationship_type = models.TextField() + object_relationship_type = models.TextField(blank=True) From c48e4745bf23b5fbce7c04d688545b263c5e09e7 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 8 Nov 2013 13:08:48 -0500 Subject: [PATCH 08/11] Fix up some bugs with signal handling related to association/disassociation --- awx/main/models/activity_stream.py | 4 ++-- awx/main/signals.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/awx/main/models/activity_stream.py b/awx/main/models/activity_stream.py index 5d4440a7e7..42b415baa4 100644 --- a/awx/main/models/activity_stream.py +++ b/awx/main/models/activity_stream.py @@ -18,11 +18,11 @@ class ActivityStream(models.Model): ('update', _("Entity Updated")), ('delete', _("Entity Deleted")), ('associate', _("Entity Associated with another Entity")), - ('disaassociate', _("Entity was Disassociated with another Entity")) + ('disassociate', _("Entity was Disassociated with another Entity")) ] user = models.ForeignKey('auth.User', null=True, on_delete=models.SET_NULL, related_name='activity_stream') - operation = models.CharField(max_length=9, choices=OPERATION_CHOICES) + operation = models.CharField(max_length=13, choices=OPERATION_CHOICES) timestamp = models.DateTimeField(auto_now_add=True) changes = models.TextField(blank=True) diff --git a/awx/main/signals.py b/awx/main/signals.py index 06abd69d81..5a6ae810c3 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -4,6 +4,7 @@ # Python import logging import threading +import json # Django from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed @@ -215,8 +216,8 @@ def activity_stream_associate(sender, instance, **kwargs): obj1_id = obj1.id obj_rel = str(sender) for entity_acted in kwargs['pk_set']: - obj2 = entity_acted - obj2_id = entity_acted.id + obj2 = kwargs['model'] + obj2_id = entity_acted activity_entry = ActivityStream( operation=action, object1_id=obj1_id, From 64d258322c2eec86e037cc65876cc6399ccbc6b1 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 11 Nov 2013 14:05:51 -0500 Subject: [PATCH 09/11] Use process_response to attach user information later in the middleware request processing --- awx/main/middleware.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/awx/main/middleware.py b/awx/main/middleware.py index 2426d58790..632cc0ed5f 100644 --- a/awx/main/middleware.py +++ b/awx/main/middleware.py @@ -3,11 +3,13 @@ from django.contrib.auth.models import User from django.db.models.signals import pre_save from django.utils.functional import curry from awx.main.models.activity_stream import ActivityStream - +import json +import urllib2 class ActivityStreamMiddleware(object): def process_request(self, request): + self.isActivityStreamEvent = False if hasattr(request, 'user') and hasattr(request.user, 'is_authenticated') and request.user.is_authenticated(): user = request.user else: @@ -18,8 +20,20 @@ class ActivityStreamMiddleware(object): def process_response(self, request, response): pre_save.disconnect(dispatch_uid=(self.__class__, request)) + if self.isActivityStreamEvent: + if "current_user" in request.COOKIES and "id" in request.COOKIES["current_user"]: + userInfo = json.loads(urllib2.unquote(request.COOKIES['current_user']).decode('utf8')) + userActual = User.objects.get(id=int(userInfo['id'])) + self.instance.user = userActual + self.instance.save() return response def set_actor(self, user, sender, instance, **kwargs): - if sender == ActivityStream and isinstance(user, User) and instance.user is None: - instance.user = user + if sender == ActivityStream: + if isinstance(user, User) and instance.user is None: + instance.user = user + else: + self.isActivityStreamEvent = True + self.instance = instance + else: + self.isActivityStreamEvent = False From aac489eea48be58ad1fb0da709954cf4abfb96d9 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 11 Nov 2013 16:37:36 -0500 Subject: [PATCH 10/11] Integrate a simple api list and detail display --- awx/api/generics.py | 5 ++++- awx/api/serializers.py | 28 ++++++++++++++++++++++++++++ awx/api/urls.py | 10 +++++----- awx/api/views.py | 10 ++++++++++ awx/main/models/activity_stream.py | 4 ++++ 5 files changed, 51 insertions(+), 6 deletions(-) diff --git a/awx/api/generics.py b/awx/api/generics.py index 89230de9b0..0787988498 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -28,7 +28,7 @@ from awx.main.utils import * # FIXME: machinery for auto-adding audit trail logs to all CREATE/EDITS -__all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'ListCreateAPIView', +__all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView', 'ListCreateAPIView', 'SubListAPIView', 'SubListCreateAPIView', 'RetrieveAPIView', 'RetrieveUpdateAPIView', 'RetrieveUpdateDestroyAPIView'] @@ -172,6 +172,9 @@ class GenericAPIView(generics.GenericAPIView, APIView): ret['search_fields'] = self.search_fields return ret +class SimpleListAPIView(generics.ListAPIView, GenericAPIView): + pass + class ListAPIView(generics.ListAPIView, GenericAPIView): # Base class for a read-only list view. diff --git a/awx/api/serializers.py b/awx/api/serializers.py index f3b0bb077e..051496b91f 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -999,6 +999,34 @@ class JobEventSerializer(BaseSerializer): pass return d +class ActivityStreamSerializer(BaseSerializer): + + class Meta: + model = ActivityStream + fields = ('id', 'url', 'related', 'summary_fields', 'timestamp', 'operation', 'changes') + + def get_related(self, obj): + if obj is None: + return {} + # res = super(ActivityStreamSerializer, self).get_related(obj) + # res.update(dict( + # #audit_trail = reverse('api:organization_audit_trail_list', args=(obj.pk,)), + # projects = reverse('api:organization_projects_list', args=(obj.pk,)), + # inventories = reverse('api:organization_inventories_list', args=(obj.pk,)), + # users = reverse('api:organization_users_list', args=(obj.pk,)), + # admins = reverse('api:organization_admins_list', args=(obj.pk,)), + # #tags = reverse('api:organization_tags_list', args=(obj.pk,)), + # teams = reverse('api:organization_teams_list', args=(obj.pk,)), + # )) + # return res + + def get_summary_fields(self, obj): + if obj is None: + return {} + d = super(ActivityStreamSerializer, self).get_summary_fields(obj) + return d + + class AuthTokenSerializer(serializers.Serializer): username = serializers.CharField() diff --git a/awx/api/urls.py b/awx/api/urls.py index 47a470aaa2..71dc9531e0 100644 --- a/awx/api/urls.py +++ b/awx/api/urls.py @@ -141,10 +141,10 @@ job_event_urls = patterns('awx.api.views', url(r'^(?P[0-9]+)/hosts/$', 'job_event_hosts_list'), ) -# activity_stream_urls = patterns('awx.api.views', -# url(r'^$', 'activity_stream_list'), -# url(r'^(?P[0-9]+)/$', 'activity_stream_detail'), -# ) +activity_stream_urls = patterns('awx.api.views', + url(r'^$', 'activity_stream_list'), + url(r'^(?P[0-9]+)/$', 'activity_stream_detail'), +) v1_urls = patterns('awx.api.views', url(r'^$', 'api_v1_root_view'), @@ -167,7 +167,7 @@ v1_urls = patterns('awx.api.views', url(r'^jobs/', include(job_urls)), url(r'^job_host_summaries/', include(job_host_summary_urls)), url(r'^job_events/', include(job_event_urls)), - # url(r'^activity_stream/', include(activity_stream_urls)), + url(r'^activity_stream/', include(activity_stream_urls)), ) urlpatterns = patterns('awx.api.views', diff --git a/awx/api/views.py b/awx/api/views.py index 05a42d512b..75896907fe 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -88,6 +88,7 @@ class ApiV1RootView(APIView): data['hosts'] = reverse('api:host_list') data['job_templates'] = reverse('api:job_template_list') data['jobs'] = reverse('api:job_list') + data['activity_stream'] = reverse('api:activity_stream_list') return Response(data) class ApiV1ConfigView(APIView): @@ -1058,6 +1059,15 @@ class JobJobEventsList(BaseJobEventsList): headers=headers) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +class ActivityStreamList(SimpleListAPIView): + + model = ActivityStream + serializer_class = ActivityStreamSerializer + +class ActivityStreamDetail(RetrieveAPIView): + + model = ActivityStream + serializer_class = ActivityStreamSerializer # Create view functions for all of the class-based views to simplify inclusion # in URL patterns and reverse URL lookups, converting CamelCase names to diff --git a/awx/main/models/activity_stream.py b/awx/main/models/activity_stream.py index 42b415baa4..b0ef9127b0 100644 --- a/awx/main/models/activity_stream.py +++ b/awx/main/models/activity_stream.py @@ -3,6 +3,7 @@ from django.db import models +from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ class ActivityStream(models.Model): @@ -33,3 +34,6 @@ class ActivityStream(models.Model): object2_type = models.TextField(null=True, blank=True) object_relationship_type = models.TextField(blank=True) + + def get_absolute_url(self): + return reverse('api:activity_stream_detail', args=(self.pk,)) From bb6f8c1f0453855672151e6b0b11f2bec3437997 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 12 Nov 2013 14:30:01 -0500 Subject: [PATCH 11/11] Make sure we show relative object information in the api --- awx/api/serializers.py | 36 ++++++++++++++++++++++++------------ awx/main/signals.py | 15 ++++++++------- awx/main/utils.py | 9 +++++++++ 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 051496b91f..cf136bff89 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -6,6 +6,7 @@ import json import re import socket import urlparse +import logging # PyYAML import yaml @@ -27,7 +28,9 @@ from rest_framework import serializers # AWX from awx.main.models import * -from awx.main.utils import update_scm_url +from awx.main.utils import update_scm_url, camelcase_to_underscore + +logger = logging.getLogger('awx.api.serializers') BASE_FIELDS = ('id', 'url', 'related', 'summary_fields', 'created', 'modified', 'name', 'description') @@ -1008,22 +1011,31 @@ class ActivityStreamSerializer(BaseSerializer): def get_related(self, obj): if obj is None: return {} - # res = super(ActivityStreamSerializer, self).get_related(obj) - # res.update(dict( - # #audit_trail = reverse('api:organization_audit_trail_list', args=(obj.pk,)), - # projects = reverse('api:organization_projects_list', args=(obj.pk,)), - # inventories = reverse('api:organization_inventories_list', args=(obj.pk,)), - # users = reverse('api:organization_users_list', args=(obj.pk,)), - # admins = reverse('api:organization_admins_list', args=(obj.pk,)), - # #tags = reverse('api:organization_tags_list', args=(obj.pk,)), - # teams = reverse('api:organization_teams_list', args=(obj.pk,)), - # )) - # return res + rel = {} + if obj.user is not None: + rel['user'] = reverse('api:user_detail', args=(obj.user.pk,)) + obj1_resolution = camelcase_to_underscore(obj.object1_type.split(".")[-1]) + rel['object_1'] = reverse('api:' + obj1_resolution + '_detail', args=(obj.object1_id,)) + if obj.operation in ('associate', 'disassociate'): + obj2_resolution = camelcase_to_underscore(obj.object2_type.split(".")[-1]) + rel['object_2'] = reverse('api:' + obj2_resolution + '_detail', args(obj.object2_id,)) + return rel def get_summary_fields(self, obj): if obj is None: return {} d = super(ActivityStreamSerializer, self).get_summary_fields(obj) + try: + obj1 = eval(obj.object1_type + ".objects.get(id=" + str(obj.object1_id) + ")") + d['object1'] = {'name': obj1.name, 'description': obj1.description} + except Exception, e: + logger.error("Error getting object 1 summary: " + str(e)) + try: + if obj.operation in ('associate', 'disassociate'): + obj2 = eval(obj.object1_type + ".objects.get(id=" + str(obj.object2_id) + ")") + d['object2'] = {'name': obj2.name, 'description': obj2.description} + except Exception, e: + logger.error("Error getting object 2 summary: " + str(e)) return d diff --git a/awx/main/signals.py b/awx/main/signals.py index 5a6ae810c3..afab9f64e6 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -12,7 +12,7 @@ from django.dispatch import receiver # AWX from awx.main.models import * -from awx.main.utils import model_instance_diff +from awx.main.utils import model_instance_diff, model_to_dict __all__ = [] @@ -178,7 +178,8 @@ def activity_stream_create(sender, instance, created, **kwargs): activity_entry = ActivityStream( operation='create', object1_id=instance.id, - object1_type=str(instance.__class__)) + object1_type=instance.__class__.__module__ + "." + instance.__class__.__name__, + changes=json.dumps(model_to_dict(instance))) activity_entry.save() def activity_stream_update(sender, instance, **kwargs): @@ -192,7 +193,7 @@ def activity_stream_update(sender, instance, **kwargs): activity_entry = ActivityStream( operation='update', object1_id=instance.id, - object1_type=str(instance.__class__), + object1_type=instance.__class__.__module__ + "." + instance.__class__.__name__, changes=json.dumps(changes)) activity_entry.save() @@ -201,7 +202,7 @@ def activity_stream_delete(sender, instance, **kwargs): activity_entry = ActivityStream( operation='delete', object1_id=instance.id, - object1_type=str(instance.__class__)) + object1_type=instance.__class__.__module__ + "." + instance.__class__.__name__) activity_entry.save() def activity_stream_associate(sender, instance, **kwargs): @@ -214,15 +215,15 @@ def activity_stream_associate(sender, instance, **kwargs): return obj1 = instance obj1_id = obj1.id - obj_rel = str(sender) + obj_rel = sender.__module__ + "." + sender.__name__ for entity_acted in kwargs['pk_set']: obj2 = kwargs['model'] obj2_id = entity_acted activity_entry = ActivityStream( operation=action, object1_id=obj1_id, - object1_type=str(obj1.__class__), + object1_type=obj1.__class__.__module__ + "." + obj1.__class__.__name__, object2_id=obj2_id, - object2_type=str(obj2.__class__), + object2_type=obj2.__module__ + "." + obj2.__name__, object_relationship_type=obj_rel) activity_entry.save() diff --git a/awx/main/utils.py b/awx/main/utils.py index 3fdfa21aa2..7665508b27 100644 --- a/awx/main/utils.py +++ b/awx/main/utils.py @@ -255,3 +255,12 @@ def model_instance_diff(old, new): diff = None return diff + +def model_to_dict(obj): + """ + Serialize a model instance to a dictionary as best as possible + """ + attr_d = {} + for field in obj._meta.fields: + attr_d[field.name] = str(getattr(obj, field.name, None)) + return attr_d