From 25704af172ce7b4cf8af4a4f45ab3b1213f42b80 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 8 Nov 2013 04:39:53 -0500 Subject: [PATCH 01/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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 From 3a02c17d2a0e1a1038da469db6215a5f4f08a116 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 8 Nov 2013 04:39:53 -0500 Subject: [PATCH 12/38] 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/registrar.py | 34 ++++++++++++++++++++++ awx/main/signals.py | 57 +++++++++++++++++++++++++++++++++++++ awx/main/utils.py | 36 +++++++++++++++++++++++ awx/settings/defaults.py | 1 + 7 files changed, 175 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/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 2a16ab38a5b76330988fce96855a8ab79f028ded Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 8 Nov 2013 04:39:53 -0500 Subject: [PATCH 13/38] Initial backend implementation for AC-25, activity stream/audit love --- awx/main/models/base.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) 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() From 2ab4cab495e765cbdee8c4ea27e96b3225177612 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 8 Nov 2013 10:04:12 -0500 Subject: [PATCH 14/38] 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 b7b22fa7f8948fe749da3a055081b1371f2c8481 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 8 Nov 2013 10:21:30 -0500 Subject: [PATCH 15/38] 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 710b01c3336d114e537ed3ada2259ec062548da2 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 8 Nov 2013 11:08:11 -0500 Subject: [PATCH 16/38] 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 a1edfe0dc28816f5bf2f4ee8dd6782985252ef7f Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 8 Nov 2013 11:16:04 -0500 Subject: [PATCH 17/38] 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 49a2502043e1d7ad5f3907779be7815a39ad85c7 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 8 Nov 2013 11:36:36 -0500 Subject: [PATCH 18/38] 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 f1de300fd707dc7ab995f389dd691c649505a805 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 8 Nov 2013 13:08:48 -0500 Subject: [PATCH 19/38] 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 e1e70b37a5bc09020ca1a4555ceaacd39a9b39d3 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 11 Nov 2013 14:05:51 -0500 Subject: [PATCH 20/38] 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 50fee995805cfd0a1e866501972dbce78e87ae91 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 11 Nov 2013 16:37:36 -0500 Subject: [PATCH 21/38] 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 1a27c75de465c3de4c7dc0b29964edbdf284d3b7 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 12 Nov 2013 14:30:01 -0500 Subject: [PATCH 22/38] 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 From 3f148e104ee548e03875798d1582fa16f351fe7a Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Wed, 13 Nov 2013 03:58:12 +0000 Subject: [PATCH 23/38] AC-625 while looking into issue surrounding non-running updates, discoverd that creating credentials from the Users and Teams tabs was not allowing the user to click Save. The Save button refused to become enabled. Turns out that when a user or team is preelected (using route params) form initialization routine needs to run and it was not. --- awx/ui/static/js/controllers/Credentials.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/awx/ui/static/js/controllers/Credentials.js b/awx/ui/static/js/controllers/Credentials.js index 87d9449899..f120a59cdc 100644 --- a/awx/ui/static/js/controllers/Credentials.js +++ b/awx/ui/static/js/controllers/Credentials.js @@ -115,7 +115,7 @@ CredentialsList.$inject = [ '$scope', '$rootScope', '$location', '$log', '$route function CredentialsAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, CredentialForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, ReturnToCaller, ClearScope, GenerateList, SearchInit, PaginateInit, LookUpInit, UserList, TeamList, GetBasePath, - GetChoices, Empty, KindChange, OwnerChange, FormSave) + GetChoices, Empty, KindChange, OwnerChange, FormSave, DebugForm) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -158,6 +158,7 @@ function CredentialsAdd ($scope, $rootScope, $compile, $location, $log, $routePa // Get the username based on incoming route scope['owner'] = 'user'; scope['user'] = $routeParams.user_id; + OwnerChange({ scope: scope }); var url = GetBasePath('users') + $routeParams.user_id + '/'; Rest.setUrl(url); Rest.get() @@ -173,6 +174,7 @@ function CredentialsAdd ($scope, $rootScope, $compile, $location, $log, $routePa // Get the username based on incoming route scope['owner'] = 'team'; scope['team'] = $routeParams.team_id; + OwnerChange({ scope: scope }); var url = GetBasePath('teams') + $routeParams.team_id + '/'; Rest.setUrl(url); Rest.get() @@ -238,7 +240,7 @@ function CredentialsAdd ($scope, $rootScope, $compile, $location, $log, $routePa CredentialsAdd.$inject = [ '$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'CredentialForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'ReturnToCaller', 'ClearScope', 'GenerateList', 'SearchInit', 'PaginateInit', 'LookUpInit', 'UserList', 'TeamList', 'GetBasePath', 'GetChoices', 'Empty', - 'KindChange', 'OwnerChange', 'FormSave']; + 'KindChange', 'OwnerChange', 'FormSave', 'DebugForm']; function CredentialsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, CredentialForm, From f2fca22e199bc665ab0ed9cc477742f0019415bf Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 13 Nov 2013 09:56:45 -0500 Subject: [PATCH 24/38] Add activitystream table migration --- awx/main/migrations/0025_v14_changes.py | 421 ++++++++++++++++++++++++ 1 file changed, 421 insertions(+) create mode 100644 awx/main/migrations/0025_v14_changes.py diff --git a/awx/main/migrations/0025_v14_changes.py b/awx/main/migrations/0025_v14_changes.py new file mode 100644 index 0000000000..ab4fcb3c3a --- /dev/null +++ b/awx/main/migrations/0025_v14_changes.py @@ -0,0 +1,421 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'ActivityStream' + db.create_table(u'main_activitystream', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='activity_stream', null=True, on_delete=models.SET_NULL, to=orm['auth.User'])), + ('operation', self.gf('django.db.models.fields.CharField')(max_length=13)), + ('timestamp', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('changes', self.gf('django.db.models.fields.TextField')(blank=True)), + ('object1_id', self.gf('django.db.models.fields.PositiveIntegerField')(db_index=True)), + ('object1_type', self.gf('django.db.models.fields.TextField')()), + ('object2_id', self.gf('django.db.models.fields.PositiveIntegerField')(null=True, db_index=True)), + ('object2_type', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + ('object_relationship_type', self.gf('django.db.models.fields.TextField')(blank=True)), + )) + db.send_create_signal('main', ['ActivityStream']) + + + def backwards(self, orm): + # Deleting model 'ActivityStream' + db.delete_table(u'main_activitystream') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'main.activitystream': { + 'Meta': {'object_name': 'ActivityStream'}, + 'changes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object1_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'object1_type': ('django.db.models.fields.TextField', [], {}), + 'object2_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'db_index': 'True'}), + 'object2_type': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'object_relationship_type': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'operation': ('django.db.models.fields.CharField', [], {'max_length': '13'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_stream'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}) + }, + 'main.authtoken': { + 'Meta': {'object_name': 'AuthToken'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'request_hash': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '40', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_tokens'", 'to': u"orm['auth.User']"}) + }, + 'main.credential': { + 'Meta': {'unique_together': "[('user', 'team', 'kind', 'name')]", 'object_name': 'Credential'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cloud': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'credential\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'kind': ('django.db.models.fields.CharField', [], {'default': "'ssh'", 'max_length': '32'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'credential\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'ssh_key_data': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'ssh_key_unlock': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'sudo_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'sudo_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'team': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'credentials'", 'null': 'True', 'blank': 'True', 'to': "orm['main.Team']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'credentials'", 'null': 'True', 'blank': 'True', 'to': u"orm['auth.User']"}), + 'username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}) + }, + 'main.group': { + 'Meta': {'unique_together': "(('name', 'inventory'),)", 'object_name': 'Group'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'group\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'groups_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_inventory_sources': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'groups'", 'blank': 'True', 'to': "orm['main.Host']"}), + 'hosts_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'groups'", 'to': "orm['main.Inventory']"}), + 'inventory_sources': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'groups'", 'blank': 'True', 'to': "orm['main.InventorySource']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'group\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'parents': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'children'", 'blank': 'True', 'to': "orm['main.Group']"}), + 'total_groups': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'total_hosts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}) + }, + 'main.host': { + 'Meta': {'unique_together': "(('name', 'inventory'),)", 'object_name': 'Host'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'host\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_inventory_sources': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instance_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts'", 'to': "orm['main.Inventory']"}), + 'inventory_sources': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'hosts'", 'blank': 'True', 'to': "orm['main.InventorySource']"}), + 'last_job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts_as_last_job+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Job']", 'blank': 'True', 'null': 'True'}), + 'last_job_host_summary': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts_as_last_job_summary+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.JobHostSummary']", 'blank': 'True', 'null': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'host\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}) + }, + 'main.inventory': { + 'Meta': {'unique_together': "[('name', 'organization')]", 'object_name': 'Inventory'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventory\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'groups_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_inventory_sources': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'hosts_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory_sources_with_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventory\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventories'", 'to': "orm['main.Organization']"}), + 'total_groups': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'total_hosts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'total_inventory_sources': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}) + }, + 'main.inventorysource': { + 'Meta': {'object_name': 'InventorySource'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventorysource\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'inventory_sources'", 'null': 'True', 'blank': 'True', 'to': "orm['main.Credential']"}), + 'current_update': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'inventory_source_as_current_update+'", 'null': 'True', 'to': "orm['main.InventoryUpdate']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'group': ('awx.main.fields.AutoOneToOneField', [], {'related_name': "'inventory_source'", 'null': 'True', 'default': 'None', 'to': "orm['main.Group']", 'blank': 'True', 'unique': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'inventory_sources'", 'null': 'True', 'to': "orm['main.Inventory']"}), + 'last_update': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'inventory_source_as_last_update+'", 'null': 'True', 'to': "orm['main.InventoryUpdate']"}), + 'last_update_failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventorysource\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'overwrite': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'overwrite_vars': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'source': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + 'source_path': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'source_regions': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'source_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'none'", 'max_length': '32', 'null': 'True'}), + 'update_interval': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'update_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'main.inventoryupdate': { + 'Meta': {'object_name': 'InventoryUpdate'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cancel_flag': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'celery_task_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventoryupdate\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory_source': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventory_updates'", 'to': "orm['main.InventorySource']"}), + 'job_args': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'job_cwd': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_env': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + 'license_error': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventoryupdate\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'result_stdout': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'result_traceback': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'new'", 'max_length': '20'}) + }, + 'main.job': { + 'Meta': {'object_name': 'Job'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cancel_flag': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'celery_task_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}), + 'cloud_credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs_as_cloud_credential+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'job\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Credential']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'jobs'", 'blank': 'True', 'through': "orm['main.JobHostSummary']", 'to': "orm['main.Host']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'job_args': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'job_cwd': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_env': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + 'job_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.JobTemplate']", 'blank': 'True', 'null': 'True'}), + 'job_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'launch_type': ('django.db.models.fields.CharField', [], {'default': "'manual'", 'max_length': '20'}), + 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'job\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'playbook': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Project']"}), + 'result_stdout': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'result_traceback': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'new'", 'max_length': '20'}), + 'verbosity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}) + }, + 'main.jobevent': { + 'Meta': {'ordering': "('pk',)", 'object_name': 'JobEvent'}, + 'changed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now_add': 'True', 'blank': 'True'}), + 'event': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'event_data': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'host': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_events_as_primary_host'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Host']", 'blank': 'True', 'null': 'True'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'job_events'", 'blank': 'True', 'to': "orm['main.Host']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_events'", 'to': "orm['main.Job']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'children'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.JobEvent']", 'blank': 'True', 'null': 'True'}), + 'play': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'task': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}) + }, + 'main.jobhostsummary': { + 'Meta': {'ordering': "('-pk',)", 'unique_together': "[('job', 'host')]", 'object_name': 'JobHostSummary'}, + 'changed': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now_add': 'True', 'blank': 'True'}), + 'dark': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'host': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_host_summaries'", 'to': "orm['main.Host']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_host_summaries'", 'to': "orm['main.Job']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'ok': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'processed': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'skipped': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}) + }, + 'main.jobtemplate': { + 'Meta': {'object_name': 'JobTemplate'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cloud_credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates_as_cloud_credential+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'jobtemplate\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'host_config_key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'job_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'jobtemplate\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'playbook': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Project']"}), + 'verbosity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}) + }, + 'main.organization': { + 'Meta': {'object_name': 'Organization'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'admins': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'admin_of_organizations'", 'blank': 'True', 'to': u"orm['auth.User']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'organization\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'organization\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': "orm['main.Project']"}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': u"orm['auth.User']"}) + }, + 'main.permission': { + 'Meta': {'object_name': 'Permission'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'permission\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'permission\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Project']"}), + 'team': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Team']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}) + }, + 'main.profile': { + 'Meta': {'object_name': 'Profile'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ldap_dn': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('awx.main.fields.AutoOneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': u"orm['auth.User']"}) + }, + 'main.project': { + 'Meta': {'object_name': 'Project'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'project\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'projects'", 'null': 'True', 'blank': 'True', 'to': "orm['main.Credential']"}), + 'current_update': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'project_as_current_update+'", 'null': 'True', 'to': "orm['main.ProjectUpdate']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_update': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'project_as_last_update+'", 'null': 'True', 'to': "orm['main.ProjectUpdate']"}), + 'last_update_failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'local_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'project\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'scm_branch': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'null': 'True', 'blank': 'True'}), + 'scm_clean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_delete_on_next_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_delete_on_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '8', 'null': 'True', 'blank': 'True'}), + 'scm_update_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'null': 'True', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'ok'", 'max_length': '32', 'null': 'True'}) + }, + 'main.projectupdate': { + 'Meta': {'object_name': 'ProjectUpdate'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cancel_flag': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'celery_task_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'projectupdate\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'job_args': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'job_cwd': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_env': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'projectupdate\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'project_updates'", 'to': "orm['main.Project']"}), + 'result_stdout': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'result_traceback': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'new'", 'max_length': '20'}) + }, + 'main.team': { + 'Meta': {'unique_together': "[('organization', 'name')]", 'object_name': 'Team'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'team\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'team\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'teams'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Organization']"}), + 'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': "orm['main.Project']"}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': u"orm['auth.User']"}) + }, + u'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'}) + }, + u'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_tagged_items'", 'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_items'", 'to': u"orm['taggit.Tag']"}) + } + } + + complete_apps = ['main'] \ No newline at end of file From d5d3495494d65a19fce15de969edde53eaab3320 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Wed, 13 Nov 2013 17:11:15 +0000 Subject: [PATCH 25/38] Found and fixed another bug on the Credentials form. Changing the ssh password value was causing a hidden password field to become invalid. Form definition for ssh password cleared the incorrect associated confirmation field. --- awx/ui/static/js/forms/Credentials.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/static/js/forms/Credentials.js b/awx/ui/static/js/forms/Credentials.js index 75d13fdd64..90f018cebd 100644 --- a/awx/ui/static/js/forms/Credentials.js +++ b/awx/ui/static/js/forms/Credentials.js @@ -139,7 +139,7 @@ angular.module('CredentialFormDefinition', []) label: 'SSH Password', type: 'password', ngShow: "kind.value == 'ssh'", - ngChange: "clearPWConfirm('password_confirm')", + ngChange: "clearPWConfirm('ssh_password_confirm')", addRequired: false, editRequired: false, ask: true, From e2b657c72cb4a2d3c7ffa60556e0f8c56e746b60 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 13 Nov 2013 13:35:13 -0500 Subject: [PATCH 26/38] Fix a bug passing args to object 2 of the activitystream serializer --- awx/api/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index cf136bff89..bb2db24313 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1018,7 +1018,7 @@ class ActivityStreamSerializer(BaseSerializer): 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,)) + rel['object_2'] = reverse('api:' + obj2_resolution + '_detail', args=(obj.object2_id,)) return rel def get_summary_fields(self, obj): From 78bdc7ae8bd0b3c7f5d95d03e657f59705c4bd86 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 13 Nov 2013 13:35:48 -0500 Subject: [PATCH 27/38] Make sure we are handling multiple activitystream instances from the middleware --- awx/main/middleware.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/awx/main/middleware.py b/awx/main/middleware.py index 632cc0ed5f..86650a9a42 100644 --- a/awx/main/middleware.py +++ b/awx/main/middleware.py @@ -1,9 +1,10 @@ from django.conf import settings from django.contrib.auth.models import User -from django.db.models.signals import pre_save +from django.db.models.signals import pre_save, post_save from django.utils.functional import curry from awx.main.models.activity_stream import ActivityStream import json +import uuid import urllib2 class ActivityStreamMiddleware(object): @@ -15,25 +16,34 @@ class ActivityStreamMiddleware(object): else: user = None + self.instances = [] set_actor = curry(self.set_actor, user) - pre_save.connect(set_actor, sender=ActivityStream, dispatch_uid=(self.__class__, request), weak=False) + self.disp_uid = str(uuid.uuid1()) + self.finished = False + post_save.connect(set_actor, sender=ActivityStream, dispatch_uid=self.disp_uid, weak=False) def process_response(self, request, response): - pre_save.disconnect(dispatch_uid=(self.__class__, request)) + post_save.disconnect(dispatch_uid=self.disp_uid) + self.finished = True 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() + for instance in self.instances: + 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'])) + instance.user = userActual + instance.save() + else: + obj1_type_actual = instance.object1_type.split(".")[-1] + if obj1_type_actual in ("InventoryUpdate", "ProjectUpdate", "JobEvent") and instance.id is not None: + instance.delete() return response def set_actor(self, user, sender, instance, **kwargs): if sender == ActivityStream: if isinstance(user, User) and instance.user is None: instance.user = user - else: + elif not self.finished: self.isActivityStreamEvent = True - self.instance = instance + self.instances.append(instance) else: self.isActivityStreamEvent = False From bd942e540a2dd92e8a38aff45eb20baafdf48f82 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 13 Nov 2013 14:43:16 -0500 Subject: [PATCH 28/38] Add job to the list of models that are filtered if a user didn't directly interface with it --- awx/main/middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/middleware.py b/awx/main/middleware.py index 86650a9a42..4906bfea26 100644 --- a/awx/main/middleware.py +++ b/awx/main/middleware.py @@ -34,7 +34,7 @@ class ActivityStreamMiddleware(object): instance.save() else: obj1_type_actual = instance.object1_type.split(".")[-1] - if obj1_type_actual in ("InventoryUpdate", "ProjectUpdate", "JobEvent") and instance.id is not None: + if obj1_type_actual in ("InventoryUpdate", "ProjectUpdate", "JobEvent", "Job") and instance.id is not None: instance.delete() return response From 9505768bf6bb963e027871e4c92ca6ae34f1632c Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Thu, 14 Nov 2013 07:02:22 +0000 Subject: [PATCH 29/38] First attempt at accessing /activity_stream. Modified /list/Stream.js to match the API and to start providing some of the translation we'll need. --- awx/ui/static/js/helpers/paginate.js | 6 ++-- awx/ui/static/js/lists/Streams.js | 19 +++++++----- awx/ui/static/js/widgets/Stream.js | 31 ++++++++++++++----- .../static/lib/ansible/generator-helpers.js | 7 +++-- 4 files changed, 43 insertions(+), 20 deletions(-) diff --git a/awx/ui/static/js/helpers/paginate.js b/awx/ui/static/js/helpers/paginate.js index 4f73081de9..acc6c228ce 100644 --- a/awx/ui/static/js/helpers/paginate.js +++ b/awx/ui/static/js/helpers/paginate.js @@ -70,9 +70,11 @@ angular.module('PaginateHelper', ['RefreshHelper', 'ngCookies']) scope[iterator + 'Page'] = 0; var new_url = url.replace(/\?page_size\=\d+/,''); - var connect = (/\/$/.test(new_url)) ? '?' : '&'; + console.log('new_url: ' + new_url); + var connect = (/\/$/.test(new_url)) ? '?' : '&'; new_url += (scope[iterator + 'SearchParams']) ? connect + scope[iterator + 'SearchParams'] + '&page_size=' + scope[iterator + 'PageSize' ] : - + connect + 'page_size=' + scope[iterator + 'PageSize' ]; + connect + 'page_size=' + scope[iterator + 'PageSize' ]; + console.log('new_url: ' + new_url); Refresh({ scope: scope, set: set, iterator: iterator, url: new_url }); } } diff --git a/awx/ui/static/js/lists/Streams.js b/awx/ui/static/js/lists/Streams.js index ff17ad1cb4..76c023ede5 100644 --- a/awx/ui/static/js/lists/Streams.js +++ b/awx/ui/static/js/lists/Streams.js @@ -19,17 +19,20 @@ angular.module('StreamListDefinition', []) "class": "table-condensed", fields: { - event_time: { - key: true, - label: 'When' - }, user: { - label: 'Who', + label: 'User', + linkTo: "\{\{ activity.userLink \}\}", sourceModel: 'user', - sourceField: 'username' + sourceField: 'username', + awToolTip: "\{\{ userToolTip \}\}", + dataPlacement: 'top' }, - operation: { - label: 'Operation' + timestamp: { + label: 'Event Time', + }, + objects: { + label: 'Objects', + ngBindHtml: 'activity.objects' }, description: { label: 'Description' diff --git a/awx/ui/static/js/widgets/Stream.js b/awx/ui/static/js/widgets/Stream.js index ee90e98645..e69aea74bb 100644 --- a/awx/ui/static/js/widgets/Stream.js +++ b/awx/ui/static/js/widgets/Stream.js @@ -61,7 +61,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti return function(params) { var list = StreamList; - var defaultUrl = $basePath + 'html/event_log.html/'; + var defaultUrl = GetBasePath('activity_stream'); var view = GenerateList; // Push the current page onto browser histor. If user clicks back button, restore current page without @@ -84,9 +84,10 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti } scope.refreshStream = function() { - scope['activities'].splice(10,10); - //scope.search(list.iterator); - } + scope.search(list.iterator); + } + + function fixUrl(u) { return u.replace(/\/api\/v1\//,'/#/'); } if (scope.removePostRefresh) { scope.removePostRefresh(); @@ -94,10 +95,26 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti scope.removePostRefresh = scope.$on('PostRefresh', function() { for (var i=0; i < scope['activities'].length; i++) { // Convert event_time date to local time zone - cDate = new Date(scope['activities'][i].event_time); - scope['activities'][i].event_time = FormatDate(cDate); + cDate = new Date(scope['activities'][i].timestamp); + scope['activities'][i].timestamp = FormatDate(cDate); // Display username - scope['activities'][i].user = scope.activities[i].summary_fields.user.username; + scope['activities'][i].user = (scope['activities'][i].summary_fields.user) ? scope['activities'][i].summary_fields.user.username : + 'System'; + if (scope['activities'][i].user !== 'System') { + scope['activities'][i].userLink = (scope['activities'][i].summary_fields.user) ? fixUrl(scope['activities'][i].related.user) : + ""; + } + + // Objects + var href; + if (scope['activities'][i].summary_fields.object1) { + href = fixUrl(scope['activities'][i].related.object_1); + scope['activities'][i].objects = "" + scope['activities'][i].summary_fields.object1.name + ""; + } + if (scope['activities'][i].summary_fields.object2) { + href = fixUrl(scope['activities'][i].related.object_2); + scope['activities'][i].objects += ", " + scope['activities'][i].summary_fields.object2.name + ""; + } } ShowStream(); }); diff --git a/awx/ui/static/lib/ansible/generator-helpers.js b/awx/ui/static/lib/ansible/generator-helpers.js index 4cff7f0b4e..1c46a9f2f8 100644 --- a/awx/ui/static/lib/ansible/generator-helpers.js +++ b/awx/ui/static/lib/ansible/generator-helpers.js @@ -340,6 +340,7 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers']) html += (field.ngClass) ? Attr(field, 'ngClass') : ""; html += (options.mode == 'lookup' || options.mode == 'select') ? " ng-click=\"toggle_" + list.iterator +"({{ " + list.iterator + ".id }})\"" : ""; html += (field.columnShow) ? Attr(field, 'columnShow') : ""; + html += (field.ngBindHtml) ? "ng-bind-html-unsafe=\"" + field.ngBindHtml + "\" " : ""; html += ">\n"; // Add ngShow @@ -359,7 +360,7 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers']) // Start the Link if ( (field.key || field.link || field.linkTo || field.ngClick || field.ngHref) && - options['mode'] != 'lookup' && options['mode'] != 'select' && !field.noLink ) { + options['mode'] != 'lookup' && options['mode'] != 'select' && !field.noLink && !field.ngBindHtml) { var cap=false; if (field.linkTo) { html += " Date: Thu, 14 Nov 2013 13:27:40 -0500 Subject: [PATCH 30/38] Implement AC-634 - Additional bits --- awx/api/serializers.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index bb2db24313..13bfbd982c 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1006,7 +1006,8 @@ class ActivityStreamSerializer(BaseSerializer): class Meta: model = ActivityStream - fields = ('id', 'url', 'related', 'summary_fields', 'timestamp', 'operation', 'changes') + fields = ('id', 'url', 'related', 'summary_fields', 'timestamp', 'operation', 'changes', + 'object1_id', 'object1_type', 'object2_id', 'object2_type', 'object_relationship_type') def get_related(self, obj): if obj is None: @@ -1015,10 +1016,10 @@ class ActivityStreamSerializer(BaseSerializer): 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,)) + rel['object1'] = 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,)) + rel['object2'] = reverse('api:' + obj2_resolution + '_detail', args=(obj.object2_id,)) return rel def get_summary_fields(self, obj): @@ -1026,14 +1027,24 @@ class ActivityStreamSerializer(BaseSerializer): return {} d = super(ActivityStreamSerializer, self).get_summary_fields(obj) try: + short_obj_type = obj.object1_type.split(".")[-1] + under_short_obj_type = camelcase_to_underscore(short_obj_type) obj1 = eval(obj.object1_type + ".objects.get(id=" + str(obj.object1_id) + ")") - d['object1'] = {'name': obj1.name, 'description': obj1.description} + d['object1'] = {'name': obj1.name, 'description': obj1.description, + 'base': under_short_obj_type, 'id': obj.object1_id} + if under_short_obj_type == "host" or under_short_obj_type == "group": + d['inventory'] = {'name': obj1.inventory.name, 'id': obj1.inventory.id} except Exception, e: logger.error("Error getting object 1 summary: " + str(e)) try: + short_obj_type = obj.object2_type.split(".")[-1] + under_short_obj_type = camelcase_to_underscore(short_obj_type) 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} + obj2 = eval(obj.object2_type + ".objects.get(id=" + str(obj.object2_id) + ")") + d['object2'] = {'name': obj2.name, 'description': obj2.description, + 'base': under_short_obj_type, 'id': obj.object2_id} + if under_short_obj_type == "host" or under_short_obj_type == "group": + d['inventory'] = {'name': obj2.inventory.name, 'id': obj2.inventory.id} except Exception, e: logger.error("Error getting object 2 summary: " + str(e)) return d From 9e8459b96a38e4a7051d31e3bb19ff31e2c8eb4c Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 14 Nov 2013 13:28:13 -0500 Subject: [PATCH 31/38] Improve loading time for some module calls by reducing the number of queries against the activity stream table --- awx/main/middleware.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/awx/main/middleware.py b/awx/main/middleware.py index 4906bfea26..87959af432 100644 --- a/awx/main/middleware.py +++ b/awx/main/middleware.py @@ -39,11 +39,12 @@ class ActivityStreamMiddleware(object): return response def set_actor(self, user, sender, instance, **kwargs): - if sender == ActivityStream: - if isinstance(user, User) and instance.user is None: - instance.user = user - elif not self.finished: - self.isActivityStreamEvent = True - self.instances.append(instance) - else: + if not self.finished: + if sender == ActivityStream: + if isinstance(user, User) and instance.user is None: + instance.user = user + else: + self.isActivityStreamEvent = True + self.instances.append(instance) + else: self.isActivityStreamEvent = False From 639b87d55ac2f0b852aaf0b69b915fc0057d7614 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 14 Nov 2013 16:50:21 -0500 Subject: [PATCH 32/38] Add some initial activity stream tests --- awx/api/serializers.py | 2 +- awx/main/tests/__init__.py | 2 +- awx/main/tests/activity_stream.py | 61 +++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 awx/main/tests/activity_stream.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 13bfbd982c..270a54556e 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1046,7 +1046,7 @@ class ActivityStreamSerializer(BaseSerializer): if under_short_obj_type == "host" or under_short_obj_type == "group": d['inventory'] = {'name': obj2.inventory.name, 'id': obj2.inventory.id} except Exception, e: - logger.error("Error getting object 2 summary: " + str(e)) + pass return d diff --git a/awx/main/tests/__init__.py b/awx/main/tests/__init__.py index 280ab9bf18..113ece1448 100644 --- a/awx/main/tests/__init__.py +++ b/awx/main/tests/__init__.py @@ -10,4 +10,4 @@ from awx.main.tests.scripts import * from awx.main.tests.tasks import RunJobTest from awx.main.tests.licenses import LicenseTests from awx.main.tests.jobs import * - +from awx.main.tests.activity_stream import * diff --git a/awx/main/tests/activity_stream.py b/awx/main/tests/activity_stream.py new file mode 100644 index 0000000000..05c1a9911f --- /dev/null +++ b/awx/main/tests/activity_stream.py @@ -0,0 +1,61 @@ +# Copyright (c) 2013 AnsibleWorks, Inc. +# All Rights Reserved. + +# Python +import contextlib +import datetime +import json +import os +import shutil +import tempfile + + +from django.contrib.auth.models import User +import django.test +from django.test.client import Client +from django.core.urlresolvers import reverse + +# AWX +from awx.main.models import * +from awx.main.tests.base import BaseTest + +class ActivityStreamTest(BaseTest): + + def collection(self): + return reverse('api:activity_stream_list') + + def item(self, item_id): + return reverse('api:activity_stream_detail', args=(item_id,)) + + def setUp(self): + super(ActivityStreamTest, self).setUp() + self.setup_users() + self.organization = self.make_organizations(self.normal_django_user, 1)[0] + self.project = self.make_projects(self.normal_django_user, 1)[0] + self.organization.projects.add(self.project) + self.organization.users.add(self.normal_django_user) + + def test_get_activity_stream_list(self): + url = self.collection() + + with self.current_user(self.normal_django_user): + self.options(url, expect=200) + self.head(url, expect=200) + response = self.get(url, expect=200) + self.check_pagination_and_size(response, 4, previous=None, next=None) + + def test_basic_fields(self): + org_item = self.item(self.organization.id) + + with self.current_user(self.super_django_user): + response = self.get(org_item, expect=200) + self.assertEqual(response['object1_id'], self.organization.id) + self.assertEqual(response['object1_type'], "awx.main.models.organization.Organization") + self.assertEqual(response['object2_id'], None) + self.assertEqual(response['object2_type'], None) + + self.assertTrue("related" in response) + self.assertTrue("object1" in response['related']) + self.assertTrue("summary_fields" in response) + self.assertTrue("object1" in response['summary_fields']) + self.assertEquals(response['summary_fields']['object1']['base'], "organization") From 8448c5b4d54fe098c4a3309b6f1fdddd553f17b5 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 14 Nov 2013 23:24:00 -0500 Subject: [PATCH 33/38] Add a field to the activity stream model that more closely identifies the api object type for aid in api querying --- awx/api/serializers.py | 16 +++++++++++----- awx/main/migrations/0025_v14_changes.py | 4 ++++ awx/main/models/activity_stream.py | 2 ++ awx/main/signals.py | 7 ++++++- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 270a54556e..ce757e1782 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1007,7 +1007,7 @@ class ActivityStreamSerializer(BaseSerializer): class Meta: model = ActivityStream fields = ('id', 'url', 'related', 'summary_fields', 'timestamp', 'operation', 'changes', - 'object1_id', 'object1_type', 'object2_id', 'object2_type', 'object_relationship_type') + 'object1_id', 'object1', 'object1_type', 'object2_id', 'object2', 'object2_type', 'object_relationship_type') def get_related(self, obj): if obj is None: @@ -1030,8 +1030,11 @@ class ActivityStreamSerializer(BaseSerializer): short_obj_type = obj.object1_type.split(".")[-1] under_short_obj_type = camelcase_to_underscore(short_obj_type) obj1 = eval(obj.object1_type + ".objects.get(id=" + str(obj.object1_id) + ")") - d['object1'] = {'name': obj1.name, 'description': obj1.description, - 'base': under_short_obj_type, 'id': obj.object1_id} + if hasattr(obj1, "name"): + d['object1'] = {'name': obj1.name, 'description': obj1.description, + 'base': under_short_obj_type, 'id': obj.object1_id} + else: + d['object1'] = {'base': under_short_obj_type, 'id': obj.object1_id} if under_short_obj_type == "host" or under_short_obj_type == "group": d['inventory'] = {'name': obj1.inventory.name, 'id': obj1.inventory.id} except Exception, e: @@ -1041,8 +1044,11 @@ class ActivityStreamSerializer(BaseSerializer): under_short_obj_type = camelcase_to_underscore(short_obj_type) if obj.operation in ('associate', 'disassociate'): obj2 = eval(obj.object2_type + ".objects.get(id=" + str(obj.object2_id) + ")") - d['object2'] = {'name': obj2.name, 'description': obj2.description, - 'base': under_short_obj_type, 'id': obj.object2_id} + if hasattr(obj2, "name"): + d['object2'] = {'name': obj2.name, 'description': obj2.description, + 'base': under_short_obj_type, 'id': obj.object2_id} + else: + d['object2'] = {'base': under_short_obj_type, 'id': obj.object2_id} if under_short_obj_type == "host" or under_short_obj_type == "group": d['inventory'] = {'name': obj2.inventory.name, 'id': obj2.inventory.id} except Exception, e: diff --git a/awx/main/migrations/0025_v14_changes.py b/awx/main/migrations/0025_v14_changes.py index ab4fcb3c3a..70fed60207 100644 --- a/awx/main/migrations/0025_v14_changes.py +++ b/awx/main/migrations/0025_v14_changes.py @@ -16,8 +16,10 @@ class Migration(SchemaMigration): ('timestamp', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), ('changes', self.gf('django.db.models.fields.TextField')(blank=True)), ('object1_id', self.gf('django.db.models.fields.PositiveIntegerField')(db_index=True)), + ('object1', self.gf('django.db.models.fields.TextField')()), ('object1_type', self.gf('django.db.models.fields.TextField')()), ('object2_id', self.gf('django.db.models.fields.PositiveIntegerField')(null=True, db_index=True)), + ('object2', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), ('object2_type', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), ('object_relationship_type', self.gf('django.db.models.fields.TextField')(blank=True)), )) @@ -70,8 +72,10 @@ class Migration(SchemaMigration): 'Meta': {'object_name': 'ActivityStream'}, 'changes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object1': ('django.db.models.fields.TextField', [], {}), 'object1_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 'object1_type': ('django.db.models.fields.TextField', [], {}), + 'object2': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 'object2_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'db_index': 'True'}), 'object2_type': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 'object_relationship_type': ('django.db.models.fields.TextField', [], {'blank': 'True'}), diff --git a/awx/main/models/activity_stream.py b/awx/main/models/activity_stream.py index b0ef9127b0..76333bd564 100644 --- a/awx/main/models/activity_stream.py +++ b/awx/main/models/activity_stream.py @@ -28,9 +28,11 @@ class ActivityStream(models.Model): changes = models.TextField(blank=True) object1_id = models.PositiveIntegerField(db_index=True) + object1 = models.TextField() object1_type = models.TextField() object2_id = models.PositiveIntegerField(db_index=True, null=True) + object2 = models.TextField(null=True, blank=True) object2_type = models.TextField(null=True, blank=True) object_relationship_type = models.TextField(blank=True) diff --git a/awx/main/signals.py b/awx/main/signals.py index afab9f64e6..44fa6447b2 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, model_to_dict +from awx.main.utils import model_instance_diff, model_to_dict, camelcase_to_underscore __all__ = [] @@ -178,6 +178,7 @@ def activity_stream_create(sender, instance, created, **kwargs): activity_entry = ActivityStream( operation='create', object1_id=instance.id, + object1=camelcase_to_underscore(instance.__class__.__name__), object1_type=instance.__class__.__module__ + "." + instance.__class__.__name__, changes=json.dumps(model_to_dict(instance))) activity_entry.save() @@ -193,6 +194,7 @@ def activity_stream_update(sender, instance, **kwargs): activity_entry = ActivityStream( operation='update', object1_id=instance.id, + object1=camelcase_to_underscore(instance.__class__.__name__), object1_type=instance.__class__.__module__ + "." + instance.__class__.__name__, changes=json.dumps(changes)) activity_entry.save() @@ -202,6 +204,7 @@ def activity_stream_delete(sender, instance, **kwargs): activity_entry = ActivityStream( operation='delete', object1_id=instance.id, + object1=camelcase_to_underscore(instance.__class__.__name__), object1_type=instance.__class__.__module__ + "." + instance.__class__.__name__) activity_entry.save() @@ -222,8 +225,10 @@ def activity_stream_associate(sender, instance, **kwargs): activity_entry = ActivityStream( operation=action, object1_id=obj1_id, + object1=camelcase_to_underscore(obj1.__class__.__name__), object1_type=obj1.__class__.__module__ + "." + obj1.__class__.__name__, object2_id=obj2_id, + object2=camelcase_to_underscore(obj2.__name__), object2_type=obj2.__module__ + "." + obj2.__name__, object_relationship_type=obj_rel) activity_entry.save() From 4b111d61c1c897da2c16b69ddfd5f9ba5a185e37 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Fri, 15 Nov 2013 16:28:54 +0000 Subject: [PATCH 34/38] AC-611 Latest activity widget features. Tweaked current filter, but still does not include an object type. Added activty stream to Organization tab. --- .../custom-theme/jquery-ui-1.10.3.custom.css | 2 +- awx/ui/static/js/app.js | 3 +- awx/ui/static/js/controllers/Home.js | 66 ++++--- awx/ui/static/js/controllers/Organizations.js | 6 +- awx/ui/static/js/forms/ActivityDetail.js | 59 ++++++ awx/ui/static/js/helpers/search.js | 24 ++- awx/ui/static/js/lists/Organizations.js | 9 + awx/ui/static/js/lists/Streams.js | 43 ++++- awx/ui/static/js/widgets/Stream.js | 182 ++++++++++++++++-- awx/ui/templates/ui/index.html | 1 + 10 files changed, 338 insertions(+), 57 deletions(-) create mode 100644 awx/ui/static/js/forms/ActivityDetail.js diff --git a/awx/ui/static/css/custom-theme/jquery-ui-1.10.3.custom.css b/awx/ui/static/css/custom-theme/jquery-ui-1.10.3.custom.css index a268ebf601..dbec9aa337 100644 --- a/awx/ui/static/css/custom-theme/jquery-ui-1.10.3.custom.css +++ b/awx/ui/static/css/custom-theme/jquery-ui-1.10.3.custom.css @@ -846,7 +846,7 @@ body .ui-tooltip { .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #e3e3e3; - background: #e5e3e3 url(/static/css/images/ui-bg_flat_75_e5e3e3_40x100.png) 50% 50% repeat-x; + background: #e5e3e3 url(/static/css/custom-theme/images/ui-bg_flat_75_e5e3e3_40x100.png) 50% 50% repeat-x; font-weight: bold; color: #005580; } diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index 7752795972..6b48bd8be0 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -84,7 +84,8 @@ angular.module('ansible', [ 'TimerService', 'StreamListDefinition', 'HomeGroupListDefinition', - 'HomeHostListDefinition' + 'HomeHostListDefinition', + 'ActivityDetailDefinition' ]) .config(['$routeProvider', function($routeProvider) { $routeProvider. diff --git a/awx/ui/static/js/controllers/Home.js b/awx/ui/static/js/controllers/Home.js index 735604c501..ba7a6bc53e 100644 --- a/awx/ui/static/js/controllers/Home.js +++ b/awx/ui/static/js/controllers/Home.js @@ -16,28 +16,33 @@ function Home ($routeParams, $scope, $rootScope, $location, Wait, ObjectCount, J ClearScope('home'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. - var waitCount = 4; - var loadedCount = 0; - - if (!$routeParams['login']) { - // If we're not logging in, start the Wait widget. Otherwise, it's already running. - Wait('start'); - } - - JobStatus({ target: 'container1' }); - InventorySyncStatus({ target: 'container2' }); - SCMSyncStatus({ target: 'container4' }); - ObjectCount({ target: 'container3' }); - - $rootScope.showActivity = function() { Stream(); } - - $rootScope.$on('WidgetLoaded', function() { - // Once all the widgets report back 'loaded', turn off Wait widget - loadedCount++; - if ( loadedCount == waitCount ) { - Wait('stop'); + var load = function() { + var waitCount = 4; + var loadedCount = 0; + + if (!$routeParams['login']) { + // If we're not logging in, start the Wait widget. Otherwise, it's already running. + Wait('start'); } - }); + + JobStatus({ target: 'container1' }); + InventorySyncStatus({ target: 'container2' }); + SCMSyncStatus({ target: 'container4' }); + ObjectCount({ target: 'container3' }); + + $rootScope.$on('WidgetLoaded', function() { + // Once all the widgets report back 'loaded', turn off Wait widget + loadedCount++; + if ( loadedCount == waitCount ) { + Wait('stop'); + } + }); + } + + $rootScope.showActivity = function() { Stream(); } + $rootScope.refresh = function() { load(); } + + load(); } Home.$inject=[ '$routeParams', '$scope', '$rootScope', '$location', 'Wait', 'ObjectCount', 'JobStatus', 'InventorySyncStatus', @@ -96,6 +101,14 @@ function HomeGroups ($location, $routeParams, HomeGroupList, GenerateList, Proce PaginateInit({ scope: scope, list: list, url: defaultUrl }); // Process search params + if ($routeParams['name']) { + scope[list.iterator + 'InputDisable'] = false; + scope[list.iterator + 'SearchValue'] = $routeParams['name']; + scope[list.iterator + 'SearchField'] = 'name'; + scope[list.iterator + 'SearchFieldLabel'] = list.fields['name'].label; + scope[list.iterator + 'SearchSelectValue'] = null; + } + if ($routeParams['has_active_failures']) { scope[list.iterator + 'InputDisable'] = true; scope[list.iterator + 'SearchValue'] = $routeParams['has_active_failures']; @@ -180,7 +193,16 @@ function HomeHosts ($location, $routeParams, HomeHostList, GenerateList, Process SearchInit({ scope: scope, set: 'hosts', list: list, url: defaultUrl }); PaginateInit({ scope: scope, list: list, url: defaultUrl }); - + + // Process search params + if ($routeParams['name']) { + scope[list.iterator + 'InputDisable'] = false; + scope[list.iterator + 'SearchValue'] = $routeParams['name']; + scope[list.iterator + 'SearchField'] = 'name'; + scope[list.iterator + 'SearchFieldLabel'] = list.fields['name'].label; + scope[list.iterator + 'SearchSelectValue'] = null; + } + if ($routeParams['has_active_failures']) { scope[HomeHostList.iterator + 'InputDisable'] = true; scope[HomeHostList.iterator + 'SearchValue'] = $routeParams['has_active_failures']; diff --git a/awx/ui/static/js/controllers/Organizations.js b/awx/ui/static/js/controllers/Organizations.js index f325f8161f..a64cfc4b61 100644 --- a/awx/ui/static/js/controllers/Organizations.js +++ b/awx/ui/static/js/controllers/Organizations.js @@ -12,7 +12,7 @@ function OrganizationsList ($routeParams, $scope, $rootScope, $location, $log, Rest, Alert, LoadBreadCrumbs, Prompt, GenerateList, OrganizationList, SearchInit, PaginateInit, ClearScope, ProcessErrors, - GetBasePath, SelectionInit, Wait) + GetBasePath, SelectionInit, Wait, Stream) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -36,6 +36,8 @@ function OrganizationsList ($routeParams, $scope, $rootScope, $location, $log, R PaginateInit({ scope: scope, list: list, url: defaultUrl }); scope.search(list.iterator); + scope.showActivity = function() { Stream(); } + scope.addOrganization = function() { $location.path($location.path() + '/add'); } @@ -70,7 +72,7 @@ function OrganizationsList ($routeParams, $scope, $rootScope, $location, $log, R OrganizationsList.$inject=[ '$routeParams', '$scope', '$rootScope', '$location', '$log', 'Rest', 'Alert', 'LoadBreadCrumbs', 'Prompt', 'GenerateList', 'OrganizationList', 'SearchInit', 'PaginateInit', 'ClearScope', 'ProcessErrors', - 'GetBasePath', 'SelectionInit', 'Wait' ]; + 'GetBasePath', 'SelectionInit', 'Wait', 'Stream']; function OrganizationsAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, OrganizationForm, diff --git a/awx/ui/static/js/forms/ActivityDetail.js b/awx/ui/static/js/forms/ActivityDetail.js new file mode 100644 index 0000000000..e6a6ba16d5 --- /dev/null +++ b/awx/ui/static/js/forms/ActivityDetail.js @@ -0,0 +1,59 @@ +/********************************************* + * Copyright (c) 2013 AnsibleWorks, Inc. + * + * ActivityDetail.js + * Form definition for Activity Stream detail + * + */ +angular.module('ActivityDetailDefinition', []) + .value( + 'ActivityDetailForm', { + + name: 'activity', + editTitle: 'Activity Detail', + well: false, + 'class': 'horizontal-narrow', + + fields: { + timestamp: { + label: 'Time', + type: 'text', + readonly: true + }, + operation: { + label: 'Operation', + type: 'text', + readonly: true + }, + object1: { + label: 'Object 1', + type: 'text', + ngHide: '!object1', + readonly: true + }, + object1_name: { + label: 'Name', + type: 'text', + ngHide: '!object1', + readonly: true + }, + object2: { + label: 'Object 2', + type: 'text', + ngHide: '!object2', + readonly: true + }, + object2_name: { + label: 'Name', + type: 'text', + ngHide: '!object2', + readonly: true + }, + changes: { + label: 'Changes', + type: 'textarea', + readonly: true + } + } + + }); //Form diff --git a/awx/ui/static/js/helpers/search.js b/awx/ui/static/js/helpers/search.js index d9ac0443cf..ecb6775ee0 100644 --- a/awx/ui/static/js/helpers/search.js +++ b/awx/ui/static/js/helpers/search.js @@ -125,7 +125,12 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) } else if (list.fields[fld].searchType && list.fields[fld].searchType == 'int') { scope[iterator + 'HideSearchType'] = true; - } + } + else if (list.fields[fld].searchType && list.fields[fld].searchType == 'isnull') { + scope[iterator + 'SearchType'] = 'isnull'; + scope[iterator + 'InputDisable'] = true; + scope[iterator + 'SearchValue'] = 'true'; + } scope.search(iterator); } @@ -187,6 +192,16 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) scope[iterator + 'SearchSelectValue'].value == null) ) { scope[iterator + 'SearchParams'] += 'iexact='; } + else if ( (list.fields[scope[iterator + 'SearchField']].searchType && + (list.fields[scope[iterator + 'SearchField']].searchType == 'or')) ) { + scope[iterator + 'SearchParams'] = ''; //start over + for (var k=0; k < list.fields[scope[iterator + 'SearchField']].searchFields.length; k++) { + scope[iterator + 'SearchParams'] += '&or__' + + list.fields[scope[iterator + 'SearchField']].searchFields[k] + + '__icontains=' + escape(scope[iterator + 'SearchValue']); + } + scope[iterator + 'SearchParams'].replace(/^\&/,''); + } else { scope[iterator + 'SearchParams'] += scope[iterator + 'SearchType'] + '='; } @@ -197,9 +212,10 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) scope[iterator + 'SearchParams'] += scope[iterator + 'SearchSelectValue'].value; } else { - //if ( list.fields[scope[iterator + 'SearchField']].searchType == undefined || - // list.fields[scope[iterator + 'SearchField']].searchType == 'gtzero' ) { - scope[iterator + 'SearchParams'] += escape(scope[iterator + 'SearchValue']); + if ( (list.fields[scope[iterator + 'SearchField']].searchType && + (list.fields[scope[iterator + 'SearchField']].searchType !== 'or')) ) { + scope[iterator + 'SearchParams'] += escape(scope[iterator + 'SearchValue']); + } } scope[iterator + 'SearchParams'] += (sort_order) ? '&order_by=' + escape(sort_order) : ''; } diff --git a/awx/ui/static/js/lists/Organizations.js b/awx/ui/static/js/lists/Organizations.js index 964a5708c7..39c6501faa 100644 --- a/awx/ui/static/js/lists/Organizations.js +++ b/awx/ui/static/js/lists/Organizations.js @@ -35,6 +35,15 @@ angular.module('OrganizationListDefinition', []) ngClick: 'addOrganization()', "class": 'btn-success btn-xs', awToolTip: 'Create a new organization' + }, + stream: { + 'class': "btn-primary btn-xs activity-btn", + ngClick: "showActivity()", + awToolTip: "View Activity Stream", + dataPlacement: "top", + icon: "icon-comments-alt", + mode: 'all', + iconSize: 'large' } }, diff --git a/awx/ui/static/js/lists/Streams.js b/awx/ui/static/js/lists/Streams.js index 76c023ede5..5fd025cd08 100644 --- a/awx/ui/static/js/lists/Streams.js +++ b/awx/ui/static/js/lists/Streams.js @@ -19,23 +19,45 @@ angular.module('StreamListDefinition', []) "class": "table-condensed", fields: { + timestamp: { + label: 'Event Time', + key: true, + desc: true, + noLink: true, + searchable: false + }, user: { label: 'User', - linkTo: "\{\{ activity.userLink \}\}", + ngBindHtml: 'activity.user', sourceModel: 'user', sourceField: 'username', awToolTip: "\{\{ userToolTip \}\}", dataPlacement: 'top' }, - timestamp: { - label: 'Event Time', - }, objects: { label: 'Objects', - ngBindHtml: 'activity.objects' + ngBindHtml: 'activity.objects', + sortField: "object1__name,object2__name", + searchable: false + }, + object_name: { + label: 'Object name', + searchOnly: true, + searchType: 'or', + searchFields: ['object1__name', 'object2__name'] }, description: { - label: 'Description' + label: 'Description', + ngBindHtml: 'activity.description', + nosort: true, + searchable: false + }, + system_event: { + label: 'System event?', + searchOnly: true, + searchType: 'isnull', + sourceModel: 'user', + sourceField: 'username' } }, @@ -61,5 +83,14 @@ angular.module('StreamListDefinition', []) }, fieldActions: { + edit: { + label: 'View', + ngClick: "showDetail(\{\{ activity.id \}\})", + icon: 'icon-zoom-in', + "class": 'btn-default btn-xs', + awToolTip: 'View event details', + dataPlacement: 'top' + } } + }); \ No newline at end of file diff --git a/awx/ui/static/js/widgets/Stream.js b/awx/ui/static/js/widgets/Stream.js index e69aea74bb..f3fd56ec3f 100644 --- a/awx/ui/static/js/widgets/Stream.js +++ b/awx/ui/static/js/widgets/Stream.js @@ -27,7 +27,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti // Try not to overlap footer. Because stream is positioned absolute, the parent // doesn't resize correctly when stream is loaded. - $('#tab-content-container').css({ 'min-height': stream.height() }); + $('#tab-content-container').css({ 'min-height': stream.height() + 50 }); // Slide in stream stream.show('slide', {'direction': 'left'}, {'duration': 500, 'queue': false }); @@ -35,9 +35,10 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti } }]) - .factory('HideStream', [ 'ClearScope', function(ClearScope) { + .factory('HideStream', [ function() { return function() { // Remove the stream widget + var stream = $('#stream-container'); stream.hide('slide', {'direction': 'left'}, {'duration': 500, 'queue': false }); @@ -54,16 +55,140 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti } }]) + .factory('FixUrl', [ function() { + return function(u) { + return u.replace(/\/api\/v1\//,'/#/'); + } + }]) + + .factory('BuildUrl', [ function() { + return function(obj) { + var url = '/#/'; + switch(obj.base) { + case 'group': + case 'host': + url += 'home/' + obj.base + 's/?name=' + obj.name; + break; + case 'inventory': + url += 'inventories/' + obj.id; + break; + default: + url += obj.base + 's/' + obj.id; + } + return url; + } + }]) + + .factory('BuildDescription', ['FixUrl', 'BuildUrl', function(FixUrl, BuildUrl) { + return function(activity) { + var descr = ''; + if (activity.summary_fields.user) { + // this is a user transaction + var usr = FixUrl(activity.related.user); + descr += 'User ' + activity.summary_fields.user.username + ' '; + } + else { + descr += 'System '; + } + descr += activity.operation; + descr += (/e$/.test(activity.operation)) ? 'd ' : 'ed '; + if (activity.summary_fields.object2) { + descr += activity.summary_fields.object2.base + ' ' + + activity.summary_fields.object2.name + '' + [ (activity.operation == 'disassociate') ? ' from ' : ' to ']; + } + if (activity.summary_fields.object1) { + descr += activity.summary_fields.object1.base + ' ' + + activity.summary_fields.object1.name + ''; + } + return descr; + } + }]) + + .factory('ShowDetail', ['Rest', 'Alert', 'GenerateForm', 'ProcessErrors', 'GetBasePath', 'FormatDate', 'ActivityDetailForm', + function(Rest, Alert, GenerateForm, ProcessErrors, GetBasePath, FormatDate, ActivityDetailForm) { + return function(activity_id) { + + var generator = GenerateForm; + var form = ActivityDetailForm; + var scope; + + var url = GetBasePath('activity_stream') + activity_id + '/'; + + // Retrieve detail record and prepopulate the form + Rest.setUrl(url); + Rest.get() + .success( function(data, status, headers, config) { + // load up the form + var results = data; + + $('#form-modal').on('show.bs.modal', function (e) { + $('#form-modal-body').css({ + width:'auto', //probably not needed + height:'auto', //probably not needed + 'max-height':'100%' + }); + }); + + //var n = results['changes'].match(/\n/g); + //var rows = (n) ? n.length : 1; + //rows = (rows < 1) ? 3 : 10; + form.fields['changes'].rows = 10; + scope = generator.inject(form, { mode: 'edit', modal: true, related: false}); + generator.reset(); + for (var fld in form.fields) { + if (results[fld]) { + if (fld == 'timestamp') { + scope[fld] = FormatDate(new Date(results[fld])); + } + else { + scope[fld] = results[fld]; + } + } + } + if (results.summary_fields.object1) { + scope['object1_name'] = results.summary_fields.object1.name; + } + if (results.summary_fields.object2) { + scope['object2_name'] = results.summary_fields.object2.name; + } + scope['changes'] = JSON.stringify(results['changes'], null, '\t'); + scope.formModalAction = function() { + $('#form-modal').modal("hide"); + } + scope.formModalActionLabel = 'OK'; + scope.formModalCancelShow = false; + scope.formModalInfo = false; + //scope.formModalHeader = results.summary_fields.project.name + ' - SCM Status'; + $('#form-modal .btn-success').removeClass('btn-success').addClass('btn-none'); + $('#form-modal').addClass('skinny-modal'); + if (!scope.$$phase) { + scope.$digest(); + } + }) + .error( function(data, status, headers, config) { + $('#form-modal').modal("hide"); + ProcessErrors(scope, data, status, form, + { hdr: 'Error!', msg: 'Failed to retrieve activity: ' + activity_id + '. GET status: ' + status }); + }); + } + }]) + .factory('Stream', ['$rootScope', '$location', 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait', 'StreamList', 'SearchInit', - 'PaginateInit', 'GenerateList', 'FormatDate', 'ShowStream', 'HideStream', + 'PaginateInit', 'GenerateList', 'FormatDate', 'ShowStream', 'HideStream', 'BuildDescription', 'FixUrl', 'ShowDetail', function($rootScope, $location, Rest, GetBasePath, ProcessErrors, Wait, StreamList, SearchInit, PaginateInit, GenerateList, - FormatDate, ShowStream, HideStream) { + FormatDate, ShowStream, HideStream, BuildDescription, FixUrl, ShowDetail) { return function(params) { var list = StreamList; var defaultUrl = GetBasePath('activity_stream'); var view = GenerateList; - + var base = $location.path().replace(/^\//,'').split('/')[0]; + + if (base !== 'home') { + var type = (base == 'inventories') ? 'inventory' : base.replace(/s$/,''); + defaultUrl += '?or__object1=' + type + '&or__object2=' + type; + } + // Push the current page onto browser histor. If user clicks back button, restore current page without // stream widget // window.history.pushState({}, "AnsibleWorks AWX", $location.path()); @@ -80,14 +205,16 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti }); scope.closeStream = function() { - HideStream(); - } + HideStream(); + } scope.refreshStream = function() { - scope.search(list.iterator); - } + scope.search(list.iterator); + } - function fixUrl(u) { return u.replace(/\/api\/v1\//,'/#/'); } + scope.showDetail = function(id) { + ShowDetail(id); + } if (scope.removePostRefresh) { scope.removePostRefresh(); @@ -97,24 +224,38 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti // Convert event_time date to local time zone cDate = new Date(scope['activities'][i].timestamp); scope['activities'][i].timestamp = FormatDate(cDate); + // Display username scope['activities'][i].user = (scope['activities'][i].summary_fields.user) ? scope['activities'][i].summary_fields.user.username : - 'System'; - if (scope['activities'][i].user !== 'System') { - scope['activities'][i].userLink = (scope['activities'][i].summary_fields.user) ? fixUrl(scope['activities'][i].related.user) : - ""; + 'system'; + if (scope['activities'][i].user !== 'system') { + // turn user into a link when not 'system' + scope['activities'][i].user = "" + + scope['activities'][i].user + ""; } - + // Objects var href; + var deleted = /^\_delete/; if (scope['activities'][i].summary_fields.object1) { - href = fixUrl(scope['activities'][i].related.object_1); - scope['activities'][i].objects = "" + scope['activities'][i].summary_fields.object1.name + ""; + if ( !deleted.test(scope['activities'][i].summary_fields.object1.name) ) { + href = FixUrl(scope['activities'][i].related.object1); + scope['activities'][i].objects = "" + scope['activities'][i].summary_fields.object1.name + ""; + } + else { + scope['activities'][i].objects = scope['activities'][i].summary_fields.object1.name; + } } if (scope['activities'][i].summary_fields.object2) { - href = fixUrl(scope['activities'][i].related.object_2); - scope['activities'][i].objects += ", " + scope['activities'][i].summary_fields.object2.name + ""; + if ( !deleted.test(scope['activities'][i].summary_fields.object2.name) ) { + href = FixUrl(scope['activities'][i].related.object2); + scope['activities'][i].objects += ", " + scope['activities'][i].summary_fields.object2.name + ""; + } + else { + scope['activities'][i].objects += scope['activities'][i].summary_fields.object2.name; + } } + scope['activities'][i].description = BuildDescription(scope['activities'][i]); } ShowStream(); }); @@ -122,8 +263,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti // Initialize search and paginate pieces and load data SearchInit({ scope: scope, set: list.name, list: list, url: defaultUrl }); PaginateInit({ scope: scope, list: list, url: defaultUrl }); - scope.search(list.iterator); - + scope.search(list.iterator); } }]); \ No newline at end of file diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index e3d5e6378a..28b6ce8dc5 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -75,6 +75,7 @@ + From ce7688d2529685470d2508c0ffb7996c59eea033 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Sun, 17 Nov 2013 06:37:34 +0000 Subject: [PATCH 35/38] Added 'working' spinner to all save/delete/select actions. This is to compensate for places where activity log is slowing down the API. AC-646 changes Rackspace credentials to show API Key in place of password. Fixed a bug on machine credential that caused username to not be passed to the API when adding and editing. --- awx/ui/static/js/controllers/Credentials.js | 13 +++++++--- awx/ui/static/js/controllers/Inventories.js | 14 +++++++--- awx/ui/static/js/controllers/JobTemplates.js | 23 +++++++++++----- awx/ui/static/js/controllers/Organizations.js | 20 +++++++++++--- awx/ui/static/js/controllers/Permissions.js | 24 ++++++++++++----- awx/ui/static/js/controllers/Projects.js | 21 ++++++++++----- awx/ui/static/js/controllers/Teams.js | 25 ++++++++++++------ awx/ui/static/js/controllers/Users.js | 26 +++++++++++++------ awx/ui/static/js/forms/Credentials.js | 17 +++++++++--- awx/ui/static/js/helpers/Credentials.js | 15 ++++++++--- awx/ui/static/js/helpers/Groups.js | 16 +++++++++--- awx/ui/static/js/helpers/Hosts.js | 18 ++++++++++--- awx/ui/static/js/helpers/JobSubmission.js | 9 +++++-- awx/ui/static/js/helpers/Selection.js | 7 +++-- awx/ui/static/js/helpers/inventory.js | 9 +++++-- awx/ui/static/js/widgets/Stream.js | 3 ++- 16 files changed, 194 insertions(+), 66 deletions(-) diff --git a/awx/ui/static/js/controllers/Credentials.js b/awx/ui/static/js/controllers/Credentials.js index f120a59cdc..b4b9c2d976 100644 --- a/awx/ui/static/js/controllers/Credentials.js +++ b/awx/ui/static/js/controllers/Credentials.js @@ -12,7 +12,7 @@ function CredentialsList ($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, CredentialList, GenerateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller, - ClearScope, ProcessErrors, GetBasePath, SelectionInit, GetChoices) + ClearScope, ProcessErrors, GetBasePath, SelectionInit, GetChoices, Wait) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -86,14 +86,17 @@ function CredentialsList ($scope, $rootScope, $location, $log, $routeParams, Res scope.deleteCredential = function(id, name) { var action = function() { + Wait('start'); var url = defaultUrl + id + '/'; Rest.setUrl(url); Rest.destroy() .success( function(data, status, headers, config) { + Wait('stop'); $('#prompt-modal').modal('hide'); scope.search(list.iterator); }) .error( function(data, status, headers, config) { + Wait('stop'); $('#prompt-modal').modal('hide'); ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); @@ -109,7 +112,7 @@ function CredentialsList ($scope, $rootScope, $location, $log, $routeParams, Res CredentialsList.$inject = [ '$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'CredentialList', 'GenerateList', 'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', - 'GetBasePath', 'SelectionInit', 'GetChoices']; + 'GetBasePath', 'SelectionInit', 'GetChoices', 'Wait' ]; function CredentialsAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, CredentialForm, @@ -360,14 +363,16 @@ function CredentialsEdit ($scope, $rootScope, $compile, $location, $log, $routeP master['secret_key'] = scope['secret_key']; break; case 'ssh': - scope['ssh_username'] = data.username; scope['ssh_password'] = data.password; - master['ssh_username'] = scope['ssh_username']; master['ssh_password'] = scope['ssh_password']; break; case 'scm': scope['scm_key_unlock'] = data['ssh_key_unlock']; break; + case 'rax': + scope['api_key'] = data['password']; + master['api_key'] = scope['api_key']; + break; } scope.$emit('credentialLoaded'); diff --git a/awx/ui/static/js/controllers/Inventories.js b/awx/ui/static/js/controllers/Inventories.js index 55fba75cec..49f441dd03 100644 --- a/awx/ui/static/js/controllers/Inventories.js +++ b/awx/ui/static/js/controllers/Inventories.js @@ -150,6 +150,7 @@ function InventoriesList ($scope, $rootScope, $location, $log, $routeParams, Res scope.deleteInventory = function(id, name) { var action = function() { + Wait('start'); var url = defaultUrl + id + '/'; $('#prompt-modal').modal('hide'); Wait('start'); @@ -207,7 +208,7 @@ InventoriesList.$inject = [ '$scope', '$rootScope', '$location', '$log', '$route function InventoriesAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, InventoryForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, ReturnToCaller, ClearScope, GenerateList, OrganizationList, SearchInit, PaginateInit, LookUpInit, GetBasePath, - ParseTypeChange) + ParseTypeChange, Wait) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -239,6 +240,7 @@ function InventoriesAdd ($scope, $rootScope, $compile, $location, $log, $routePa // Save scope.formSave = function() { generator.clearApiErrors(); + Wait('start'); try { // Make sure we have valid variable data if (scope.inventoryParseType == 'json') { @@ -273,23 +275,28 @@ function InventoriesAdd ($scope, $rootScope, $compile, $location, $log, $routePa Rest.setUrl(data.related.variable_data); Rest.put(json_data) .success( function(data, status, headers, config) { + Wait('stop'); $location.path('/inventories/' + inventory_id + '/groups'); }) .error( function(data, status, headers, config) { + Wait('stop'); ProcessErrors(scope, data, status, form, { hdr: 'Error!', msg: 'Failed to add inventory varaibles. PUT returned status: ' + status }); }); } else { + Wait('stop'); $location.path('/inventories/' + inventory_id + '/groups'); } }) .error( function(data, status, headers, config) { + Wait('stop'); ProcessErrors(scope, data, status, form, { hdr: 'Error!', msg: 'Failed to add new inventory. Post returned status: ' + status }); }); } catch(err) { + Wait('stop'); Alert("Error", "Error parsing inventory variables. Parser returned: " + err); } @@ -304,13 +311,14 @@ function InventoriesAdd ($scope, $rootScope, $compile, $location, $log, $routePa InventoriesAdd.$inject = [ '$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'InventoryForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'ReturnToCaller', 'ClearScope', 'GenerateList', - 'OrganizationList', 'SearchInit', 'PaginateInit', 'LookUpInit', 'GetBasePath', 'ParseTypeChange']; + 'OrganizationList', 'SearchInit', 'PaginateInit', 'LookUpInit', 'GetBasePath', 'ParseTypeChange', 'Wait']; function InventoriesEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, InventoryForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, RelatedSearchInit, RelatedPaginateInit, ReturnToCaller, ClearScope, LookUpInit, Prompt, OrganizationList, - GetBasePath, LoadInventory, ParseTypeChange, EditInventory, SaveInventory, PostLoadInventory) + GetBasePath, LoadInventory, ParseTypeChange, EditInventory, SaveInventory, PostLoadInventory + ) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. diff --git a/awx/ui/static/js/controllers/JobTemplates.js b/awx/ui/static/js/controllers/JobTemplates.js index 653401672b..7dfaf36bc6 100644 --- a/awx/ui/static/js/controllers/JobTemplates.js +++ b/awx/ui/static/js/controllers/JobTemplates.js @@ -13,7 +13,7 @@ function JobTemplatesList ($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, JobTemplateList, GenerateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, PromptPasswords, JobTemplateForm, CredentialList, - LookUpInit, SubmitJob) + LookUpInit, SubmitJob, Wait) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -49,14 +49,17 @@ function JobTemplatesList ($scope, $rootScope, $location, $log, $routeParams, Re scope.deleteJobTemplate = function(id, name) { var action = function() { + Wait('start'); var url = defaultUrl + id + '/'; Rest.setUrl(url); Rest.destroy() .success( function(data, status, headers, config) { + Wait('stop'); $('#prompt-modal').modal('hide'); scope.search(list.iterator); }) .error( function(data, status, headers, config) { + Wait('stop'); $('#prompt-modal').modal('hide'); ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); @@ -77,13 +80,13 @@ function JobTemplatesList ($scope, $rootScope, $location, $log, $routeParams, Re JobTemplatesList.$inject = [ '$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'JobTemplateList', 'GenerateList', 'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors','GetBasePath', 'PromptPasswords', 'JobTemplateForm', 'CredentialList', 'LookUpInit', - 'SubmitJob' + 'SubmitJob', 'Wait' ]; function JobTemplatesAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, JobTemplateForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, ReturnToCaller, ClearScope, GetBasePath, InventoryList, CredentialList, ProjectList, LookUpInit, - md5Setup, ParseTypeChange) + md5Setup, ParseTypeChange, Wait) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -224,6 +227,7 @@ function JobTemplatesAdd ($scope, $rootScope, $compile, $location, $log, $routeP // Save scope.formSave = function() { generator.clearApiErrors(); + Wait('start'); var data = {} try { // Make sure we have valid variable data @@ -258,16 +262,19 @@ function JobTemplatesAdd ($scope, $rootScope, $compile, $location, $log, $routeP Rest.setUrl(defaultUrl); Rest.post(data) .success( function(data, status, headers, config) { + Wait('stop'); var base = $location.path().replace(/^\//,'').split('/')[0]; (base == 'job_templates') ? ReturnToCaller() : ReturnToCaller(1); }) .error( function(data, status, headers, config) { + Wait('stop'); ProcessErrors(scope, data, status, form, { hdr: 'Error!', msg: 'Failed to add new job template. POST returned status: ' + status }); }); } catch(err) { + Wait('stop'); Alert("Error", "Error parsing extra variables. Parser returned: " + err); } }; @@ -286,14 +293,14 @@ function JobTemplatesAdd ($scope, $rootScope, $compile, $location, $log, $routeP JobTemplatesAdd.$inject = [ '$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'JobTemplateForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'ReturnToCaller', 'ClearScope', 'GetBasePath', 'InventoryList', 'CredentialList', 'ProjectList', 'LookUpInit', - 'md5Setup', 'ParseTypeChange' ]; + 'md5Setup', 'ParseTypeChange', 'Wait']; function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, JobTemplateForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, RelatedSearchInit, RelatedPaginateInit, ReturnToCaller, ClearScope, InventoryList, CredentialList, ProjectList, LookUpInit, PromptPasswords, GetBasePath, md5Setup, ParseTypeChange, - JobStatusToolTip, FormatDate) + JobStatusToolTip, FormatDate, Wait) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -530,6 +537,7 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route // Save changes to the parent scope.formSave = function() { generator.clearApiErrors(); + Wait('start'); var data = {} try { // Make sure we have valid variable data @@ -564,16 +572,19 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route Rest.setUrl(defaultUrl + id + '/'); Rest.put(data) .success( function(data, status, headers, config) { + Wait('stop'); var base = $location.path().replace(/^\//,'').split('/')[0]; (base == 'job_templates') ? ReturnToCaller() : ReturnToCaller(1); }) .error( function(data, status, headers, config) { + Wait('stop'); ProcessErrors(scope, data, status, form, { hdr: 'Error!', msg: 'Failed to update job template. PUT returned status: ' + status }); }); } catch(err) { + Wait('stop'); Alert("Error", "Error parsing extra variables. Parser returned: " + err); } }; @@ -631,5 +642,5 @@ JobTemplatesEdit.$inject = [ '$scope', '$rootScope', '$compile', '$location', '$ 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'RelatedSearchInit', 'RelatedPaginateInit', 'ReturnToCaller', 'ClearScope', 'InventoryList', 'CredentialList', 'ProjectList', 'LookUpInit', 'PromptPasswords', 'GetBasePath', 'md5Setup', 'ParseTypeChange', - 'JobStatusToolTip', 'FormatDate' + 'JobStatusToolTip', 'FormatDate', 'Wait' ]; diff --git a/awx/ui/static/js/controllers/Organizations.js b/awx/ui/static/js/controllers/Organizations.js index a64cfc4b61..743311cc8f 100644 --- a/awx/ui/static/js/controllers/Organizations.js +++ b/awx/ui/static/js/controllers/Organizations.js @@ -49,14 +49,17 @@ function OrganizationsList ($routeParams, $scope, $rootScope, $location, $log, R scope.deleteOrganization = function(id, name) { var action = function() { + Wait('start'); var url = defaultUrl + id + '/'; Rest.setUrl(url); Rest.destroy() .success( function(data, status, headers, config) { + Wait('stop'); $('#prompt-modal').modal('hide'); scope.search(list.iterator); }) .error( function(data, status, headers, config) { + Wait('stop'); $('#prompt-modal').modal('hide'); ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); @@ -77,7 +80,7 @@ OrganizationsList.$inject=[ '$routeParams', '$scope', '$rootScope', '$location', function OrganizationsAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, OrganizationForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, ClearScope, GetBasePath, - ReturnToCaller) + ReturnToCaller, Wait) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -94,12 +97,14 @@ function OrganizationsAdd ($scope, $rootScope, $compile, $location, $log, $route // Save scope.formSave = function() { form.clearApiErrors(); + Wait('start'); var url = GetBasePath(base); url += (base != 'organizations') ? $routeParams['project_id'] + '/organizations/' : ''; Rest.setUrl(url); Rest.post({ name: $scope.name, description: $scope.description }) .success( function(data, status, headers, config) { + Wait('stop'); if (base == 'organizations') { $rootScope.flashMessage = "New organization successfully created!"; $location.path('/organizations/' + data.id); @@ -109,6 +114,7 @@ function OrganizationsAdd ($scope, $rootScope, $compile, $location, $log, $route } }) .error( function(data, status, headers, config) { + Wait('stop'); ProcessErrors(scope, data, status, OrganizationForm, { hdr: 'Error!', msg: 'Failed to add new organization. Post returned status: ' + status }); }); @@ -123,12 +129,12 @@ function OrganizationsAdd ($scope, $rootScope, $compile, $location, $log, $route OrganizationsAdd.$inject = [ '$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'OrganizationForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'ClearScope', 'GetBasePath', - 'ReturnToCaller' ]; + 'ReturnToCaller', 'Wait']; function OrganizationsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, OrganizationForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, RelatedSearchInit, - RelatedPaginateInit, Prompt, ClearScope, GetBasePath) + RelatedPaginateInit, Prompt, ClearScope, GetBasePath, Wait) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -186,6 +192,7 @@ function OrganizationsEdit ($scope, $rootScope, $compile, $location, $log, $rout // Save changes to the parent scope.formSave = function() { generator.clearApiErrors(); + Wait('start'); var params = {}; for (var fld in form.fields) { params[fld] = scope[fld]; @@ -193,10 +200,12 @@ function OrganizationsEdit ($scope, $rootScope, $compile, $location, $log, $rout Rest.setUrl(defaultUrl + id + '/'); Rest.put(params) .success( function(data, status, headers, config) { + Wait('stop'); master = params; $rootScope.flashMessage = "Your changes were successfully saved!"; }) .error( function(data, status, headers, config) { + Wait('stop'); ProcessErrors(scope, data, status, OrganizationForm, { hdr: 'Error!', msg: 'Failed to update organization: ' + id + '. PUT status: ' + status }); }); @@ -228,14 +237,17 @@ function OrganizationsEdit ($scope, $rootScope, $compile, $location, $log, $rout $rootScope.flashMessage = null; var action = function() { + Wait('start'); var url = defaultUrl + $routeParams.organization_id + '/' + set + '/'; Rest.setUrl(url); Rest.post({ id: itm_id, disassociate: 1 }) .success( function(data, status, headers, config) { + Wait('stop'); $('#prompt-modal').modal('hide'); scope.search(form.related[set].iterator); }) .error( function(data, status, headers, config) { + Wait('stop'); $('#prompt-modal').modal('hide'); ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + ' failed. POST returned status: ' + status }); @@ -252,4 +264,4 @@ function OrganizationsEdit ($scope, $rootScope, $compile, $location, $log, $rout OrganizationsEdit.$inject = [ '$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'OrganizationForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'RelatedSearchInit', - 'RelatedPaginateInit', 'Prompt', 'ClearScope', 'GetBasePath']; + 'RelatedPaginateInit', 'Prompt', 'ClearScope', 'GetBasePath', 'Wait']; diff --git a/awx/ui/static/js/controllers/Permissions.js b/awx/ui/static/js/controllers/Permissions.js index 2a081cfb7e..78f3453e72 100644 --- a/awx/ui/static/js/controllers/Permissions.js +++ b/awx/ui/static/js/controllers/Permissions.js @@ -1,7 +1,7 @@ function PermissionsList ($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, PermissionList, GenerateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller, - ClearScope, ProcessErrors, GetBasePath, CheckAccess) + ClearScope, ProcessErrors, GetBasePath, CheckAccess, Wait) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -35,14 +35,17 @@ function PermissionsList ($scope, $rootScope, $location, $log, $routeParams, Res scope.deletePermission = function(id, name) { var action = function() { + Wait('start'); var url = GetBasePath('base') + 'permissions/' + id + '/'; Rest.setUrl(url); Rest.destroy() .success( function(data, status, headers, config) { + Wait('stop'); $('#prompt-modal').modal('hide'); scope.search(list.iterator); }) .error( function(data, status, headers, config) { + Wait('stop'); $('#prompt-modal').modal('hide'); ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); @@ -60,13 +63,14 @@ function PermissionsList ($scope, $rootScope, $location, $log, $routeParams, Res PermissionsList.$inject = [ '$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'PermissionList', 'GenerateList', 'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', - 'ClearScope', 'ProcessErrors', 'GetBasePath', 'CheckAccess' + 'ClearScope', 'ProcessErrors', 'GetBasePath', 'CheckAccess', 'Wait' ]; function PermissionsAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, PermissionsForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, ClearScope, - GetBasePath, ReturnToCaller, InventoryList, ProjectList, LookUpInit, CheckAccess) + GetBasePath, ReturnToCaller, InventoryList, ProjectList, LookUpInit, CheckAccess, + Wait) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -110,6 +114,7 @@ function PermissionsAdd ($scope, $rootScope, $compile, $location, $log, $routePa // Save scope.formSave = function() { generator.clearApiErrors(); + Wait('start'); if (scope.PermissionAddAllowed) { var data = {}; for (var fld in form.fields) { @@ -119,9 +124,11 @@ function PermissionsAdd ($scope, $rootScope, $compile, $location, $log, $routePa Rest.setUrl(url); Rest.post(data) .success( function(data, status, headers, config) { + Wait('stop'); ReturnToCaller(1); }) .error( function(data, status, headers, config) { + Wait('stop'); ProcessErrors(scope, data, status, PermissionsForm, { hdr: 'Error!', msg: 'Failed to create new permission. Post returned status: ' + status }); }); @@ -153,13 +160,14 @@ function PermissionsAdd ($scope, $rootScope, $compile, $location, $log, $routePa PermissionsAdd.$inject = [ '$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'PermissionsForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'ClearScope', 'GetBasePath', - 'ReturnToCaller', 'InventoryList', 'ProjectList', 'LookUpInit', 'CheckAccess' + 'ReturnToCaller', 'InventoryList', 'ProjectList', 'LookUpInit', 'CheckAccess', 'Wait' ]; function PermissionsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, PermissionsForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, ReturnToCaller, - ClearScope, Prompt, GetBasePath, InventoryList, ProjectList, LookUpInit, CheckAccess) + ClearScope, Prompt, GetBasePath, InventoryList, ProjectList, LookUpInit, CheckAccess, + Wait) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -247,6 +255,7 @@ function PermissionsEdit ($scope, $rootScope, $compile, $location, $log, $routeP // Save changes to the parent scope.formSave = function() { generator.clearApiErrors(); + Wait('start'); var data = {} for (var fld in form.fields) { data[fld] = scope[fld]; @@ -254,9 +263,11 @@ function PermissionsEdit ($scope, $rootScope, $compile, $location, $log, $routeP Rest.setUrl(defaultUrl); Rest.put(data) .success( function(data, status, headers, config) { + Wait('stop'); ReturnToCaller(1); }) .error( function(data, status, headers, config) { + Wait('stop'); ProcessErrors(scope, data, status, form, { hdr: 'Error!', msg: 'Failed to update Permission: ' + $routeParams.id + '. PUT status: ' + status }); }); @@ -286,6 +297,7 @@ function PermissionsEdit ($scope, $rootScope, $compile, $location, $log, $routeP PermissionsEdit.$inject = [ '$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'PermissionsForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'ReturnToCaller', - 'ClearScope', 'Prompt', 'GetBasePath', 'InventoryList', 'ProjectList', 'LookUpInit', 'CheckAccess' + 'ClearScope', 'Prompt', 'GetBasePath', 'InventoryList', 'ProjectList', 'LookUpInit', 'CheckAccess', + 'Wait' ]; diff --git a/awx/ui/static/js/controllers/Projects.js b/awx/ui/static/js/controllers/Projects.js index fa13092afe..3b610d4a3e 100644 --- a/awx/ui/static/js/controllers/Projects.js +++ b/awx/ui/static/js/controllers/Projects.js @@ -13,7 +13,7 @@ function ProjectsList ($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, ProjectList, GenerateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, SelectionInit, ProjectUpdate, ProjectStatus, - FormatDate, Refresh) + FormatDate, Refresh, Wait) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -112,14 +112,17 @@ function ProjectsList ($scope, $rootScope, $location, $log, $routeParams, Rest, scope.deleteProject = function(id, name) { var action = function() { + Wait('start'); var url = defaultUrl + id + '/'; Rest.setUrl(url); Rest.destroy() .success( function(data, status, headers, config) { + Wait('stop'); $('#prompt-modal').modal('hide'); scope.search(list.iterator); }) .error( function(data, status, headers, config) { + Wait('stop'); $('#prompt-modal').modal('hide'); ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); @@ -225,13 +228,13 @@ function ProjectsList ($scope, $rootScope, $location, $log, $routeParams, Rest, ProjectsList.$inject = [ '$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'ProjectList', 'GenerateList', 'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', - 'GetBasePath', 'SelectionInit', 'ProjectUpdate', 'ProjectStatus', 'FormatDate', 'Refresh' ]; + 'GetBasePath', 'SelectionInit', 'ProjectUpdate', 'ProjectStatus', 'FormatDate', 'Refresh', 'Wait' ]; function ProjectsAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, ProjectsForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, ClearScope, GetBasePath, ReturnToCaller, GetProjectPath, LookUpInit, OrganizationList, - CredentialList, GetChoices, DebugForm) + CredentialList, GetChoices, DebugForm, Wait) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -291,6 +294,7 @@ function ProjectsAdd ($scope, $rootScope, $compile, $location, $log, $routeParam // Save scope.formSave = function() { generator.clearApiErrors(); + Wait('start'); var data = {}; for (var fld in form.fields) { if (form.fields[fld].type == 'checkbox_group') { @@ -322,6 +326,7 @@ function ProjectsAdd ($scope, $rootScope, $compile, $location, $log, $routeParam Rest.setUrl(url); Rest.post(org) .success( function(data, status, headers, config) { + Wait('stop'); $rootScope.flashMessage = "New project successfully created!"; (base == 'projects') ? ReturnToCaller() : ReturnToCaller(1); }) @@ -331,6 +336,7 @@ function ProjectsAdd ($scope, $rootScope, $compile, $location, $log, $routeParam }); }) .error( function(data, status, headers, config) { + Wait('stop'); ProcessErrors(scope, data, status, ProjectsForm, { hdr: 'Error!', msg: 'Failed to create new project. POST returned status: ' + status }); }); @@ -357,14 +363,14 @@ function ProjectsAdd ($scope, $rootScope, $compile, $location, $log, $routeParam ProjectsAdd.$inject = [ '$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'ProjectsForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'ClearScope', 'GetBasePath', 'ReturnToCaller', 'GetProjectPath', 'LookUpInit', 'OrganizationList', 'CredentialList', 'GetChoices', - 'DebugForm' + 'DebugForm', 'Wait' ]; function ProjectsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, ProjectsForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, RelatedSearchInit, RelatedPaginateInit, Prompt, ClearScope, GetBasePath, ReturnToCaller, GetProjectPath, - Authorization, CredentialList, LookUpInit, GetChoices, Empty, DebugForm) + Authorization, CredentialList, LookUpInit, GetChoices, Empty, DebugForm, Wait) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -499,6 +505,7 @@ function ProjectsEdit ($scope, $rootScope, $compile, $location, $log, $routePara // Save changes to the parent scope.formSave = function() { generator.clearApiErrors(); + Wait('start'); $rootScope.flashMessage = null; var params = {}; for (var fld in form.fields) { @@ -525,9 +532,11 @@ function ProjectsEdit ($scope, $rootScope, $compile, $location, $log, $routePara Rest.setUrl(defaultUrl); Rest.put(params) .success( function(data, status, headers, config) { + Wait('stop'); ReturnToCaller(); }) .error( function(data, status, headers, config) { + Wait('stop'); ProcessErrors(scope, data, status, form, { hdr: 'Error!', msg: 'Failed to update project: ' + id + '. PUT status: ' + status }); }); @@ -591,5 +600,5 @@ ProjectsEdit.$inject = [ '$scope', '$rootScope', '$compile', '$location', '$log' 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'RelatedSearchInit', 'RelatedPaginateInit', 'Prompt', 'ClearScope', 'GetBasePath', 'ReturnToCaller', 'GetProjectPath', 'Authorization', 'CredentialList', 'LookUpInit', 'GetChoices', 'Empty', - 'DebugForm' + 'DebugForm', 'Wait' ]; diff --git a/awx/ui/static/js/controllers/Teams.js b/awx/ui/static/js/controllers/Teams.js index b5e66cfe7f..9d5279f145 100644 --- a/awx/ui/static/js/controllers/Teams.js +++ b/awx/ui/static/js/controllers/Teams.js @@ -12,7 +12,7 @@ function TeamsList ($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, TeamList, GenerateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller, - ClearScope, ProcessErrors, SetTeamListeners, GetBasePath, SelectionInit) + ClearScope, ProcessErrors, SetTeamListeners, GetBasePath, SelectionInit, Wait) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -56,14 +56,17 @@ function TeamsList ($scope, $rootScope, $location, $log, $routeParams, Rest, Ale scope.deleteTeam = function(id, name) { var action = function() { + Wait('start'); var url = defaultUrl + id + '/'; Rest.setUrl(url); Rest.destroy() .success( function(data, status, headers, config) { + Wait('stop'); $('#prompt-modal').modal('hide'); scope.search(list.iterator); }) .error( function(data, status, headers, config) { + Wait('stop'); $('#prompt-modal').modal('hide'); ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); @@ -90,12 +93,12 @@ function TeamsList ($scope, $rootScope, $location, $log, $routeParams, Rest, Ale TeamsList.$inject = [ '$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'TeamList', 'GenerateList', 'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', - 'SetTeamListeners', 'GetBasePath', 'SelectionInit']; + 'SetTeamListeners', 'GetBasePath', 'SelectionInit', 'Wait']; -function TeamsAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, TeamForm, - GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, ReturnToCaller, ClearScope, - GenerateList, OrganizationList, SearchInit, PaginateInit, GetBasePath, LookUpInit) +function TeamsAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, TeamForm, GenerateForm, + Rest, Alert, ProcessErrors, LoadBreadCrumbs, ReturnToCaller, ClearScope, GenerateList, + OrganizationList, SearchInit, PaginateInit, GetBasePath, LookUpInit, Wait) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -120,6 +123,7 @@ function TeamsAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, // Save scope.formSave = function() { generator.clearApiErrors(); + Wait('start'); Rest.setUrl(defaultUrl); var data = {} for (var fld in form.fields) { @@ -127,10 +131,12 @@ function TeamsAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, } Rest.post(data) .success( function(data, status, headers, config) { + Wait('stop'); $rootScope.flashMessage = "New team successfully created!"; $location.path('/teams/' + data.id); }) .error( function(data, status, headers, config) { + Wait('stop'); ProcessErrors(scope, data, status, form, { hdr: 'Error!', msg: 'Failed to add new team. Post returned status: ' + status }); }); @@ -145,13 +151,13 @@ function TeamsAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, TeamsAdd.$inject = [ '$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'TeamForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'ReturnToCaller', 'ClearScope', 'GenerateList', - 'OrganizationList', 'SearchInit', 'PaginateInit', 'GetBasePath', 'LookUpInit' ]; + 'OrganizationList', 'SearchInit', 'PaginateInit', 'GetBasePath', 'LookUpInit', 'Wait']; function TeamsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, TeamForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, RelatedSearchInit, RelatedPaginateInit, ReturnToCaller, ClearScope, LookUpInit, Prompt, - GetBasePath, CheckAccess, OrganizationList) + GetBasePath, CheckAccess, OrganizationList, Wait) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -230,6 +236,7 @@ function TeamsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, // Save changes to the parent scope.formSave = function() { generator.clearApiErrors(); + Wait('start'); $rootScope.flashMessage = null; Rest.setUrl(defaultUrl + $routeParams.team_id +'/'); var data = {} @@ -238,10 +245,12 @@ function TeamsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, } Rest.put(data) .success( function(data, status, headers, config) { + Wait('stop'); var base = $location.path().replace(/^\//,'').split('/')[0]; (base == 'teams') ? ReturnToCaller() : ReturnToCaller(1); }) .error( function(data, status, headers, config) { + Wait('stop'); ProcessErrors(scope, data, status, form, { hdr: 'Error!', msg: 'Failed to update team: ' + $routeParams.team_id + '. PUT status: ' + status }); }); @@ -335,6 +344,6 @@ function TeamsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, TeamsEdit.$inject = [ '$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'TeamForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'RelatedSearchInit', 'RelatedPaginateInit', 'ReturnToCaller', 'ClearScope', 'LookUpInit', 'Prompt', - 'GetBasePath', 'CheckAccess', 'OrganizationList' + 'GetBasePath', 'CheckAccess', 'OrganizationList', 'Wait' ]; diff --git a/awx/ui/static/js/controllers/Users.js b/awx/ui/static/js/controllers/Users.js index 79dd2d8794..a0e73ce685 100644 --- a/awx/ui/static/js/controllers/Users.js +++ b/awx/ui/static/js/controllers/Users.js @@ -10,9 +10,9 @@ 'use strict'; -function UsersList ($scope, $rootScope, $location, $log, $routeParams, Rest, - Alert, UserList, GenerateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, - ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, SelectionInit) +function UsersList ($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, UserList, + GenerateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller, + ClearScope, ProcessErrors, GetBasePath, SelectionInit, Wait) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -47,14 +47,17 @@ function UsersList ($scope, $rootScope, $location, $log, $routeParams, Rest, scope.deleteUser = function(id, name) { var action = function() { + Wait('start') var url = defaultUrl + id + '/'; Rest.setUrl(url); Rest.destroy() .success( function(data, status, headers, config) { + Wait('stop'); $('#prompt-modal').modal('hide'); scope.search(list.iterator); }) .error( function(data, status, headers, config) { + Wait('stop'); $('#prompt-modal').modal('hide'); ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); @@ -70,12 +73,12 @@ function UsersList ($scope, $rootScope, $location, $log, $routeParams, Rest, UsersList.$inject = [ '$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'UserList', 'GenerateList', 'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', - 'GetBasePath', 'SelectionInit']; + 'GetBasePath', 'SelectionInit', 'Wait' ]; function UsersAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, UserForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, ReturnToCaller, ClearScope, - GetBasePath, LookUpInit, OrganizationList, ResetForm) + GetBasePath, LookUpInit, OrganizationList, ResetForm, Wait) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -121,6 +124,7 @@ function UsersAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, // Save scope.formSave = function() { generator.clearApiErrors(); + Wait('start'); if (scope.organization !== undefined && scope.organization !== null && scope.organization !== '') { Rest.setUrl(defaultUrl + scope.organization + '/users/'); var data = {} @@ -138,6 +142,7 @@ function UsersAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, Rest.post(data) .success( function(data, status, headers, config) { + Wait('stop'); var base = $location.path().replace(/^\//,'').split('/')[0]; if (base == 'users') { $rootScope.flashMessage = 'New user successfully created!'; @@ -148,6 +153,7 @@ function UsersAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, } }) .error( function(data, status, headers, config) { + Wait('stop'); ProcessErrors(scope, data, status, form, { hdr: 'Error!', msg: 'Failed to add new user. POST returned status: ' + status }); }); @@ -174,12 +180,13 @@ function UsersAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, UsersAdd.$inject = [ '$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'UserForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'ReturnToCaller', 'ClearScope', 'GetBasePath', - 'LookUpInit', 'OrganizationList', 'ResetForm' ]; + 'LookUpInit', 'OrganizationList', 'ResetForm', 'Wait' ]; function UsersEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, UserForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, RelatedSearchInit, - RelatedPaginateInit, ReturnToCaller, ClearScope, GetBasePath, Prompt, CheckAccess, ResetForm) + RelatedPaginateInit, ReturnToCaller, ClearScope, GetBasePath, Prompt, CheckAccess, + ResetForm, Wait) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -252,6 +259,7 @@ function UsersEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, // Save changes to the parent scope.formSave = function() { generator.clearApiErrors(); + Wait('start'); $rootScope.flashMessage = null; Rest.setUrl(defaultUrl + id + '/'); var data = {} @@ -269,10 +277,12 @@ function UsersEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, Rest.put(data) .success( function(data, status, headers, config) { + Wait('stop'); var base = $location.path().replace(/^\//,'').split('/')[0]; (base == 'users') ? ReturnToCaller() : ReturnToCaller(1); }) .error( function(data, status, headers, config) { + Wait('stop'); ProcessErrors(scope, data, status, form, { hdr: 'Error!', msg: 'Failed to update users: ' + $routeParams.id + '. PUT status: ' + status }); }); @@ -425,5 +435,5 @@ function UsersEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, UsersEdit.$inject = [ '$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'UserForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'RelatedSearchInit', 'RelatedPaginateInit', 'ReturnToCaller', 'ClearScope', 'GetBasePath', 'Prompt', 'CheckAccess', - 'ResetForm' ]; + 'ResetForm', 'Wait' ]; diff --git a/awx/ui/static/js/forms/Credentials.js b/awx/ui/static/js/forms/Credentials.js index 90f018cebd..495a557e17 100644 --- a/awx/ui/static/js/forms/Credentials.js +++ b/awx/ui/static/js/forms/Credentials.js @@ -114,11 +114,22 @@ angular.module('CredentialFormDefinition', []) awRequiredWhen: {variable: 'rackspace_required', init: false }, autocomplete: false }, + "api_key": { + label: 'API Key', + type: 'password', + ngShow: "kind.value == 'rax'", + awRequiredWhen: { variable: "rackspace_required", init: false }, + autocomplete: false, + ask: false, + clear: false, + apiField: 'passwowrd' + }, "password": { label: 'Password', type: 'password', - ngShow: "kind.value == 'rax' || kind.value == 'scm'", - awRequiredWhen: {variable: 'rackspace_required', init: false }, + ngShow: "kind.value == 'scm'", + addRequired: false, + editRequired: false, ngChange: "clearPWConfirm('password_confirm')", ask: false, clear: false, @@ -128,7 +139,7 @@ angular.module('CredentialFormDefinition', []) "password_confirm": { label: 'Confirm Password', type: 'password', - ngShow: "kind.value == 'rax' || kind.value == 'scm'", + ngShow: "kind.value == 'scm'", addRequired: false, editRequired: false, awPassMatch: true, diff --git a/awx/ui/static/js/helpers/Credentials.js b/awx/ui/static/js/helpers/Credentials.js index b021981dfc..1f33533f3b 100644 --- a/awx/ui/static/js/helpers/Credentials.js +++ b/awx/ui/static/js/helpers/Credentials.js @@ -44,6 +44,7 @@ angular.module('CredentialsHelper', ['Utilities']) if (reset) { scope['access_key'] = null; scope['secret_key'] = null; + scope['api_key'] = null; scope['username'] = null; scope['password'] = null; scope['password_confirm'] = null; @@ -94,14 +95,16 @@ angular.module('CredentialsHelper', ['Utilities']) }]) - .factory('FormSave', ['$location', 'Rest', 'ProcessErrors', 'Empty', 'GetBasePath', 'CredentialForm', 'ReturnToCaller', - function($location, Rest, ProcessErrors, Empty, GetBasePath, CredentialForm, ReturnToCaller) { + .factory('FormSave', ['$location', 'Rest', 'ProcessErrors', 'Empty', 'GetBasePath', 'CredentialForm', 'ReturnToCaller', 'Wait', + function($location, Rest, ProcessErrors, Empty, GetBasePath, CredentialForm, ReturnToCaller, Wait) { return function(params) { var scope = params.scope; var mode = params.mode; // add or edit var form = CredentialForm; var data = {} + Wait('start'); + for (var fld in form.fields) { if (fld !== 'access_key' && fld !== 'secret_key' && fld !== 'ssh_username' && fld !== 'ssh_password') { @@ -127,7 +130,6 @@ angular.module('CredentialsHelper', ['Utilities']) switch (data['kind']) { case 'ssh': - data['username'] = scope['ssh_username']; data['password'] = scope['ssh_password']; break; case 'aws': @@ -137,6 +139,9 @@ angular.module('CredentialsHelper', ['Utilities']) case 'scm': data['ssh_key_unlock'] = scope['scm_key_unlock']; break; + case 'rax': + data['password'] = scope['api_key']; + break; } if (Empty(data.team) && Empty(data.user)) { @@ -150,10 +155,12 @@ angular.module('CredentialsHelper', ['Utilities']) Rest.setUrl(url); Rest.post(data) .success( function(data, status, headers, config) { + Wait('stop'); var base = $location.path().replace(/^\//,'').split('/')[0]; (base == 'credentials') ? ReturnToCaller() : ReturnToCaller(1); }) .error( function(data, status, headers, config) { + Wait('stop'); ProcessErrors(scope, data, status, form, { hdr: 'Error!', msg: 'Failed to create new Credential. POST status: ' + status }); }); @@ -163,10 +170,12 @@ angular.module('CredentialsHelper', ['Utilities']) Rest.setUrl(url); Rest.put(data) .success( function(data, status, headers, config) { + Wait('stop'); var base = $location.path().replace(/^\//,'').split('/')[0]; (base == 'credentials') ? ReturnToCaller() : ReturnToCaller(1); }) .error( function(data, status, headers, config) { + Wait('stop'); ProcessErrors(scope, data, status, form, { hdr: 'Error!', msg: 'Failed to update Credential. PUT status: ' + status }); }); diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js index 56ba78810f..3c43b6555f 100644 --- a/awx/ui/static/js/helpers/Groups.js +++ b/awx/ui/static/js/helpers/Groups.js @@ -564,9 +564,9 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' }]) .factory('GroupsAdd', ['$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'GroupForm', 'GenerateForm', - 'Prompt', 'ProcessErrors', 'GetBasePath', 'ParseTypeChange', 'GroupsEdit', 'BuildTree', 'ClickNode', + 'Prompt', 'ProcessErrors', 'GetBasePath', 'ParseTypeChange', 'GroupsEdit', 'BuildTree', 'ClickNode', 'Wait', function($rootScope, $location, $log, $routeParams, Rest, Alert, GroupForm, GenerateForm, Prompt, ProcessErrors, - GetBasePath, ParseTypeChange, GroupsEdit, BuildTree, ClickNode) { + GetBasePath, ParseTypeChange, GroupsEdit, BuildTree, ClickNode, Wait) { return function(params) { var inventory_id = params.inventory_id; @@ -603,6 +603,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' // Save scope.formModalAction = function() { + Wait('start'); try { scope.formModalActionDisabled = true; @@ -638,6 +639,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' Rest.setUrl(defaultUrl); Rest.post(data) .success( function(data, status, headers, config) { + Wait('stop'); groupCreated = true; scope.formModalActionDisabled = false; scope.showGroupHelp = false; //get rid of the Hint @@ -653,12 +655,14 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' }); }) .error( function(data, status, headers, config) { + Wait('stop'); scope.formModalActionDisabled = false; ProcessErrors(scope, data, status, form, { hdr: 'Error!', msg: 'Failed to add new group. POST returned status: ' + status }); }); } catch(err) { + Wait('stop'); scope.formModalActionDisabled = false; Alert("Error", "Error parsing group variables. Parser returned: " + err); } @@ -675,10 +679,10 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' .factory('GroupsEdit', ['$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'GroupForm', 'GenerateForm', 'Prompt', 'ProcessErrors', 'GetBasePath', 'SetNodeName', 'ParseTypeChange', 'GetSourceTypeOptions', 'InventoryUpdate', - 'GetUpdateIntervalOptions', 'ClickNode', 'LookUpInit', 'CredentialList', 'Empty', + 'GetUpdateIntervalOptions', 'ClickNode', 'LookUpInit', 'CredentialList', 'Empty', 'Wait', function($rootScope, $location, $log, $routeParams, Rest, Alert, GroupForm, GenerateForm, Prompt, ProcessErrors, GetBasePath, SetNodeName, ParseTypeChange, GetSourceTypeOptions, InventoryUpdate, GetUpdateIntervalOptions, ClickNode, - LookUpInit, CredentialList, Empty) { + LookUpInit, CredentialList, Empty, Wait) { return function(params) { var group_id = params.group_id; @@ -942,6 +946,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' // Save changes to the parent scope.formSave = function() { + Wait('start'); try { var refreshHosts = false; @@ -967,6 +972,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' Rest.setUrl(defaultUrl); Rest.put(data) .success( function(data, status, headers, config) { + Wait('stop'); if (scope.variables) { //update group variables Rest.setUrl(scope.variable_url); @@ -979,11 +985,13 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' scope.$emit('formSaveSuccess', data.id); }) .error( function(data, status, headers, config) { + Wait('stop'); ProcessErrors(scope, data, status, form, { hdr: 'Error!', msg: 'Failed to update group: ' + group_id + '. PUT status: ' + status }); }); } catch(err) { + Wait('stop'); Alert("Error", "Error parsing group variables. Parser returned: " + err); } }; diff --git a/awx/ui/static/js/helpers/Hosts.js b/awx/ui/static/js/helpers/Hosts.js index bf1d246e20..31b6056eba 100644 --- a/awx/ui/static/js/helpers/Hosts.js +++ b/awx/ui/static/js/helpers/Hosts.js @@ -161,9 +161,9 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H .factory('HostsAdd', ['$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'HostForm', 'GenerateForm', - 'Prompt', 'ProcessErrors', 'GetBasePath', 'HostsReload', 'ParseTypeChange', + 'Prompt', 'ProcessErrors', 'GetBasePath', 'HostsReload', 'ParseTypeChange', 'Wait', function($rootScope, $location, $log, $routeParams, Rest, Alert, HostForm, GenerateForm, Prompt, ProcessErrors, - GetBasePath, HostsReload, ParseTypeChange) { + GetBasePath, HostsReload, ParseTypeChange, Wait) { return function(params) { var inventory_id = params.inventory_id; @@ -202,6 +202,8 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H // Save scope.formModalAction = function() { + Wait('start'); + function finished() { $('#form-modal').modal('hide'); scope.$emit('hostsReload'); @@ -245,15 +247,18 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H Rest.setUrl(defaultUrl); Rest.post(data) .success( function(data, status, headers, config) { + Wait('stop'); finished(); }) .error( function(data, status, headers, config) { + Wait('stop'); scope.formModalActionDisabled = false; ProcessErrors(scope, data, status, form, { hdr: 'Error!', msg: 'Failed to add new host. POST returned status: ' + status }); }); } catch(err) { + Wait('stop'); scope.formModalActionDisabled = false; Alert("Error", "Error parsing host variables. Parser returned: " + err); } @@ -270,9 +275,9 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H .factory('HostsEdit', ['$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'HostForm', 'GenerateForm', - 'Prompt', 'ProcessErrors', 'GetBasePath', 'HostsReload', 'ParseTypeChange', + 'Prompt', 'ProcessErrors', 'GetBasePath', 'HostsReload', 'ParseTypeChange', 'Wait', function($rootScope, $location, $log, $routeParams, Rest, Alert, HostForm, GenerateForm, Prompt, ProcessErrors, - GetBasePath, HostsReload, ParseTypeChange) { + GetBasePath, HostsReload, ParseTypeChange, Wait) { return function(params) { var host_id = params.host_id; @@ -366,6 +371,8 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H // Save changes to the parent scope.formModalAction = function() { + Wait('start'); + function finished() { $('#form-modal').modal('hide'); if (hostsReload) { @@ -406,14 +413,17 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H Rest.setUrl(defaultUrl); Rest.put(data) .success( function(data, status, headers, config) { + Wait('stop'); finished(); }) .error( function(data, status, headers, config) { + Wait('stop'); ProcessErrors(scope, data, status, form, { hdr: 'Error!', msg: 'Failed to update host: ' + host_id + '. PUT returned status: ' + status }); }); } catch(err) { + Wait('stop'); Alert("Error", "Error parsing host variables. Parser returned: " + err); } }; diff --git a/awx/ui/static/js/helpers/JobSubmission.js b/awx/ui/static/js/helpers/JobSubmission.js index f997706292..a64c242f28 100644 --- a/awx/ui/static/js/helpers/JobSubmission.js +++ b/awx/ui/static/js/helpers/JobSubmission.js @@ -8,8 +8,9 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential 'LookUpHelper', 'ProjectFormDefinition', 'JobSubmissionHelper', 'GroupFormDefinition', 'GroupsHelper' ]) .factory('PromptPasswords', ['CredentialForm', 'JobTemplateForm', 'GroupForm', 'ProjectsForm', '$compile', 'Rest', '$location', 'ProcessErrors', - 'GetBasePath', 'Alert', 'Empty', - function(CredentialForm, JobTemplateForm, ProjectsForm, GroupForm, $compile, Rest, $location, ProcessErrors, GetBasePath, Alert, Empty) { + 'GetBasePath', 'Alert', 'Empty', 'Wait', + function(CredentialForm, JobTemplateForm, ProjectsForm, GroupForm, $compile, Rest, $location, ProcessErrors, GetBasePath, Alert, Empty, + Wait) { return function(params) { var scope = params.scope; @@ -65,6 +66,7 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential scope.startJob = function() { $('#password-modal').modal('hide'); + Wait('start'); var pswd = {}; var value_supplied = false; $('.password-field').each(function(index) { @@ -77,17 +79,20 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential Rest.setUrl(start_url); Rest.post(pswd) .success( function(data, status, headers, config) { + Wait('stop'); scope.$emit('UpdateSubmitted','started'); if (form.name == 'credential') { navigate(false); } }) .error( function(data, status, headers, config) { + Wait('stop'); ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'POST to ' + start_url + ' failed with status: ' + status }); }); } else { + Wait('stop'); Alert('No Passwords', 'Required password(s) not provided. The request was not submitted.', 'alert-info'); if (form.name == 'credential') { // No passwords provided, so we can't start the job. Rather than leave the job in a 'new' diff --git a/awx/ui/static/js/helpers/Selection.js b/awx/ui/static/js/helpers/Selection.js index 4fd1b66f4b..92367f2375 100644 --- a/awx/ui/static/js/helpers/Selection.js +++ b/awx/ui/static/js/helpers/Selection.js @@ -12,8 +12,8 @@ angular.module('SelectionHelper', ['Utilities', 'RestServices']) - .factory('SelectionInit', [ 'Rest', 'Alert', 'ProcessErrors', 'ReturnToCaller', - function(Rest, Alert, ProcessErrors, ReturnToCaller) { + .factory('SelectionInit', [ 'Rest', 'Alert', 'ProcessErrors', 'ReturnToCaller', 'Wait', + function(Rest, Alert, ProcessErrors, ReturnToCaller, Wait) { return function(params) { var scope = params.scope; // current scope @@ -74,6 +74,8 @@ angular.module('SelectionHelper', ['Utilities', 'RestServices']) scope.queue = []; scope.formModalActionDisabled = true; + Wait('start'); + function finished() { scope.selected = []; if (returnToCaller !== undefined) { @@ -92,6 +94,7 @@ angular.module('SelectionHelper', ['Utilities', 'RestServices']) // We call the API for each selected item. We need to hang out until all the api // calls are finished. if (scope.queue.length == scope.selected.length) { + Wait('stop'); var errors = 0; for (var i=0; i < scope.queue.length; i++) { if (scope.queue[i].result == 'error') { diff --git a/awx/ui/static/js/helpers/inventory.js b/awx/ui/static/js/helpers/inventory.js index 0cf040a43e..b0df8c0e2c 100644 --- a/awx/ui/static/js/helpers/inventory.js +++ b/awx/ui/static/js/helpers/inventory.js @@ -106,9 +106,9 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi }]) .factory('SaveInventory', ['InventoryForm', 'Rest', 'Alert', 'ProcessErrors', 'LookUpInit', 'OrganizationList', - 'GetBasePath', 'ParseTypeChange', 'LoadInventory', + 'GetBasePath', 'ParseTypeChange', 'LoadInventory', 'Wait', function(InventoryForm, Rest, Alert, ProcessErrors, LookUpInit, OrganizationList, GetBasePath, ParseTypeChange, - LoadInventory) { + LoadInventory, Wait) { return function(params) { // Save inventory property modifications @@ -116,6 +116,8 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi var scope = params.scope; var form = InventoryForm; var defaultUrl=GetBasePath('inventory'); + + Wait('start'); try { // Make sure we have valid variable data @@ -150,6 +152,7 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi Rest.setUrl(data.related.variable_data); Rest.put(json_data) .success( function(data, status, headers, config) { + Wait('stop'); scope.$emit('inventorySaved'); }) .error( function(data, status, headers, config) { @@ -162,11 +165,13 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi } }) .error( function(data, status, headers, config) { + Wait('stop'); ProcessErrors(scope, data, status, form, { hdr: 'Error!', msg: 'Failed to update inventory. POST returned status: ' + status }); }); } catch(err) { + Wait('stop'); Alert("Error", "Error parsing inventory variables. Parser returned: " + err); } } diff --git a/awx/ui/static/js/widgets/Stream.js b/awx/ui/static/js/widgets/Stream.js index f3fd56ec3f..498fef4a3b 100644 --- a/awx/ui/static/js/widgets/Stream.js +++ b/awx/ui/static/js/widgets/Stream.js @@ -195,6 +195,8 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti // Add a container for the stream widget $('#tab-content-container').append('
'); + + ShowStream(); // Generate the list var scope = view.inject(list, { @@ -257,7 +259,6 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti } scope['activities'][i].description = BuildDescription(scope['activities'][i]); } - ShowStream(); }); // Initialize search and paginate pieces and load data From 7952cb560b6d51c237cca05e309e1c0b8ce45826 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Sun, 17 Nov 2013 06:58:11 +0000 Subject: [PATCH 36/38] AC-640 Added additional help text explaining why an organization is required on a project. --- awx/ui/static/js/forms/Projects.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/awx/ui/static/js/forms/Projects.js b/awx/ui/static/js/forms/Projects.js index f54f83f6b3..4e15fb1154 100644 --- a/awx/ui/static/js/forms/Projects.js +++ b/awx/ui/static/js/forms/Projects.js @@ -42,7 +42,9 @@ angular.module('ProjectFormDefinition', []) ngClick: 'lookUpOrganization()', awRequiredWhen: {variable: "organizationrequired", init: "true" }, awPopOver: '

A project must have at least one organization. Pick one organization now to create the project, and then after ' + - 'the project is created you can add additional organizations.' , + 'the project is created you can add additional organizations.

Only super users and organization administrators are allowed ' + + 'to make changes to projects. Associating one or more organizations to a project determins which organizations admins have ' + + 'access to modify the project.', dataTitle: 'Organization', dataContainer: 'body', dataPlacement: 'right' From c720e5d51cfe141d1e120f0d33c1626696e5493a Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Mon, 18 Nov 2013 08:16:51 +0000 Subject: [PATCH 37/38] Search.js now supports up to 3 search filters on a page. First search widget can be regular or an object selector, which is used on the activity stream widget. Activity stream now has 2 filter widgets allowing a search to combine object and responsible user in a single query. --- awx/ui/static/js/controllers/Home.js | 9 +- awx/ui/static/js/forms/ActivityDetail.js | 5 + awx/ui/static/js/helpers/refresh.js | 5 + awx/ui/static/js/helpers/search.js | 459 +++++++++++------- awx/ui/static/js/lists/Streams.js | 75 ++- awx/ui/static/js/widgets/Stream.js | 26 +- awx/ui/static/lib/ansible/directives.js | 14 + .../static/lib/ansible/generator-helpers.js | 117 +++-- awx/ui/static/lib/ansible/list-generator.js | 4 +- 9 files changed, 482 insertions(+), 232 deletions(-) diff --git a/awx/ui/static/js/controllers/Home.js b/awx/ui/static/js/controllers/Home.js index ba7a6bc53e..f3bb8ac9bc 100644 --- a/awx/ui/static/js/controllers/Home.js +++ b/awx/ui/static/js/controllers/Home.js @@ -196,11 +196,10 @@ function HomeHosts ($location, $routeParams, HomeHostList, GenerateList, Process // Process search params if ($routeParams['name']) { - scope[list.iterator + 'InputDisable'] = false; - scope[list.iterator + 'SearchValue'] = $routeParams['name']; - scope[list.iterator + 'SearchField'] = 'name'; - scope[list.iterator + 'SearchFieldLabel'] = list.fields['name'].label; - scope[list.iterator + 'SearchSelectValue'] = null; + scope[HomeHostList.iterator + 'InputDisable'] = false; + scope[HomeHostListiterator + 'SearchValue'] = $routeParams['name']; + scope[HomeHostList.iterator + 'SearchField'] = 'name'; + scope[lHomeHostList.iterator + 'SearchFieldLabel'] = list.fields['name'].label; } if ($routeParams['has_active_failures']) { diff --git a/awx/ui/static/js/forms/ActivityDetail.js b/awx/ui/static/js/forms/ActivityDetail.js index e6a6ba16d5..b5cadc53d3 100644 --- a/awx/ui/static/js/forms/ActivityDetail.js +++ b/awx/ui/static/js/forms/ActivityDetail.js @@ -20,6 +20,11 @@ angular.module('ActivityDetailDefinition', []) type: 'text', readonly: true }, + id: { + label: 'Event ID', + type: 'text', + readonly: true + }, operation: { label: 'Operation', type: 'text', diff --git a/awx/ui/static/js/helpers/refresh.js b/awx/ui/static/js/helpers/refresh.js index 0a6c29d0c1..5cbc3b95be 100644 --- a/awx/ui/static/js/helpers/refresh.js +++ b/awx/ui/static/js/helpers/refresh.js @@ -33,11 +33,16 @@ angular.module('RefreshHelper', ['RestServices', 'Utilities']) scope[iterator + 'PageCount'] = Math.ceil((data.count / scope[iterator + 'PageSize'])); scope[iterator + 'SearchSpin'] = false; scope[iterator + 'Loading'] = false; + for (var i=1; i <= 3; i++) { + var modifier = (i == 1) ? '' : i; + scope[iterator + 'HoldInput' + modifier] = false; + } scope[set] = data['results']; scope.$emit('PostRefresh'); }) .error ( function(data, status, headers, config) { scope[iterator + 'SearchSpin'] = false; + scope[iterator + 'HoldInput'] = false; ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve ' + set + '. GET returned status: ' + status }); }); diff --git a/awx/ui/static/js/helpers/search.js b/awx/ui/static/js/helpers/search.js index ecb6775ee0..a638c524c3 100644 --- a/awx/ui/static/js/helpers/search.js +++ b/awx/ui/static/js/helpers/search.js @@ -16,7 +16,8 @@ */ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) - .factory('SearchInit', ['Alert', 'Rest', 'Refresh', '$location', function(Alert, Rest, Refresh, $location) { + .factory('SearchInit', ['Alert', 'Rest', 'Refresh', '$location', 'GetBasePath', 'Empty', '$timeout', + function(Alert, Rest, Refresh, $location, GetBasePath, Empty, $timeout) { return function(params) { var scope = params.scope; @@ -25,227 +26,355 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) var list = params.list; var iterator = (params.iterator) ? params.iterator : list.iterator; var sort_order; + + if (scope.searchTimer) { + $timeout.cancel(scope.searchTimer); + } - function setDefaults() { + function setDefaults(widget) { // Set default values + var modifier = (widget == undefined || widget == 1) ? '' : widget; + scope[iterator + 'SearchField' + modifier] = ''; + scope[iterator + 'SearchFieldLabel' + modifier] = ''; for (fld in list.fields) { - if (list.fields[fld].key) { - if (list.fields[fld].sourceModel) { - var fka = list.fields[fld].sourceModel + '__' + list.fields[fld].sourceField; - sort_order = (list.fields[fld].desc) ? '-' + fka : fka; - } - else { - sort_order = (list.fields[fld].desc) ? '-' + fld : fld; - } - if (list.fields[fld].searchable == undefined || list.fields[fld].searchable == true) { - scope[iterator + 'SearchField'] = fld; - scope[iterator + 'SearchFieldLabel'] = list.fields[fld].label; - } - break; + if (list.fields[fld].searchWidget == undefined && widget == 1 || + list.fields[fld].searchWidget == widget) { + if (list.fields[fld].key) { + if (list.fields[fld].sourceModel) { + var fka = list.fields[fld].sourceModel + '__' + list.fields[fld].sourceField; + sort_order = (list.fields[fld].desc) ? '-' + fka : fka; + } + else { + sort_order = (list.fields[fld].desc) ? '-' + fld : fld; + } + if (list.fields[fld].searchable == undefined || list.fields[fld].searchable == true) { + scope[iterator + 'SearchField' + modifier] = fld; + scope[iterator + 'SearchFieldLabel' + modifier] = list.fields[fld].label; + } + break; + } } } - if (!scope[iterator + 'SearchField']) { - // A field marked as key may not be 'searchable' + if (Empty(scope[iterator + 'SearchField' + modifier])) { + // A field marked as key may not be 'searchable'. Find the first searchable field. for (fld in list.fields) { - if (list.fields[fld].searchable == undefined || list.fields[fld].searchable == true) { - scope[iterator + 'SearchField'] = fld; - scope[iterator + 'SearchFieldLabel'] = list.fields[fld].label; - break; + if (list.fields[fld].searchWidget == undefined && widget == 1 || + list.fields[fld].searchWidget == widget) { + if (list.fields[fld].searchable == undefined || list.fields[fld].searchable == true) { + scope[iterator + 'SearchField' + modifier] = fld; + scope[iterator + 'SearchFieldLabel' + modifier] = list.fields[fld].label; + break; + } } } } - scope[iterator + 'SearchType'] = 'icontains'; - scope[iterator + 'SearchTypeLabel'] = 'Contains'; - scope[iterator + 'SearchParams'] = ''; - scope[iterator + 'SearchValue'] = ''; - scope[iterator + 'SelectShow'] = false; // show/hide the Select - scope[iterator + 'HideSearchType'] = false; - scope[iterator + 'InputDisable'] = false; - scope[iterator + 'ExtraParms'] = ''; + scope[iterator + 'SearchType' + modifier] = 'icontains'; + scope[iterator + 'SearchTypeLabel' + modifier] = 'Contains'; + scope[iterator + 'SearchParams' + modifier] = ''; + scope[iterator + 'SearchValue' + modifier] = ''; + scope[iterator + 'SelectShow' + modifier] = false; // show/hide the Select + scope[iterator + 'HideSearchType' + modifier] = false; + scope[iterator + 'InputDisable' + modifier] = false; + scope[iterator + 'ExtraParms' + modifier] = ''; + + scope[iterator + 'SearchPlaceholder' + modifier] = + (list.fields[scope[iterator + 'SearchField' + modifier]] && + list.fields[scope[iterator + 'SearchField' + modifier]].searchPlaceholder) ? + list.fields[scope[iterator + 'SearchField' + modifier]].searchPlaceholder : 'Search'; + + scope[iterator + 'InputDisable' + modifier] = + (list.fields[scope[iterator + 'SearchField' + modifier]] && + list.fields[scope[iterator + 'SearchField' + modifier]].searchObject == 'all') ? true : false; - var f = scope[iterator + 'SearchField'] - if (list.fields[f].searchType && ( list.fields[f].searchType == 'boolean' - || list.fields[f].searchType == 'select')) { - scope[iterator + 'SelectShow'] = true; - scope[iterator + 'SearchSelectOpts'] = list.fields[f].searchOptions; - } - if (list.fields[f].searchType && list.fields[f].searchType == 'int') { - scope[iterator + 'HideSearchType'] = true; - } - if (list.fields[f].searchType && list.fields[f].searchType == 'gtzero') { - scope[iterator + "InputHide"] = true; + var f = scope[iterator + 'SearchField' + modifier]; + if (list.fields[f]) { + if ( list.fields[f].searchType && (list.fields[f].searchType == 'boolean' + || list.fields[f].searchType == 'select') ) { + scope[iterator + 'SelectShow' + modifier] = true; + scope[iterator + 'SearchSelectOpts' + modifier] = list.fields[f].searchOptions; + } + if (list.fields[f].searchType && list.fields[f].searchType == 'int') { + scope[iterator + 'HideSearchType' + modifier] = true; + } + if (list.fields[f].searchType && list.fields[f].searchType == 'gtzero') { + scope[iterator + 'InputHide' + modifier] = true; + } } } - setDefaults(); + for (var i=1; i <= 3; i++) { + var modifier = (i == 1) ? '' : i; + if ( $('#search-widget-container' + modifier) ) { + setDefaults(i); + } + } // Functions to handle search widget changes - scope.setSearchField = function(iterator, fld, label) { - scope[iterator + 'SearchFieldLabel'] = label; - scope[iterator + 'SearchField'] = fld; - scope[iterator + 'SearchValue'] = ''; - scope[iterator + 'SelectShow'] = false; - scope[iterator + 'HideSearchType'] = false; - scope[iterator + 'InputHide'] = false; - scope[iterator + 'InputDisable'] = false; - scope[iterator + 'SearchType'] = 'icontains'; + scope.setSearchField = function(iterator, fld, label, widget) { + + var modifier = (widget == undefined || widget == 1) ? '' : widget; + scope[iterator + 'SearchFieldLabel' + modifier] = label; + scope[iterator + 'SearchField' + modifier] = fld; + scope[iterator + 'SearchValue' + modifier] = ''; + scope[iterator + 'SelectShow' + modifier] = false; + scope[iterator + 'HideSearchType' + modifier] = false; + scope[iterator + 'InputHide' + modifier] = false; + scope[iterator + 'SearchType' + modifier] = 'icontains'; + scope[iterator + 'SearchPlaceholder' + modifier] = (list.fields[fld].searchPlaceholder) ? list.fields[fld].searchPlaceholder : 'Search'; + scope[iterator + 'InputDisable' + modifier] = (list.fields[fld].searchObject == 'all') ? true : false; if (list.fields[fld].searchType && list.fields[fld].searchType == 'gtzero') { - scope[iterator + "InputDisable"] = true; + scope[iterator + "InputDisable" + modifier] = true; } else if (list.fields[fld].searchSingleValue){ // Query a specific attribute for one specific value // searchSingleValue: true // searchType: 'boolean|int|etc.' // searchValue: < value to match for boolean use 'true'|'false' > - scope[iterator + 'InputDisable'] = true; - scope[iterator + "SearchValue"] = list.fields[fld].searchValue; + scope[iterator + 'InputDisable' + modifier] = true; + scope[iterator + "SearchValue" + modifier] = list.fields[fld].searchValue; // For boolean type, SearchValue must be an object if (list.fields[fld].searchType == 'boolean' && list.fields[fld].searchValue == 'true') { - scope[iterator + "SearchSelectValue"] = { value: 1 }; + scope[iterator + "SearchSelectValue" + modifier] = { value: 1 }; } else if (list.fields[fld].searchType == 'boolean' && list.fields[fld].searchValue == 'false') { - scope[iterator + "SearchSelectValue"] = { value: 0 }; + scope[iterator + "SearchSelectValue" + modifier] = { value: 0 }; } else { - scope[iterator + "SearchSelectValue"] = { value: list.fields[fld].searchValue }; + scope[iterator + "SearchSelectValue" + modifier] = { value: list.fields[fld].searchValue }; } } else if (list.fields[fld].searchType == 'in') { - scope[iterator + "SearchType"] = 'in'; - scope[iterator + "SearchValue"] = list.fields[fld].searchValue; - scope[iterator + "InputDisable"] = true; + scope[iterator + "SearchType" + modifier] = 'in'; + scope[iterator + "SearchValue" + modifier] = list.fields[fld].searchValue; + scope[iterator + "InputDisable" + modifier] = true; } else if (list.fields[fld].searchType && (list.fields[fld].searchType == 'boolean' - || list.fields[fld].searchType == 'select')) { - scope[iterator + 'SelectShow'] = true; - scope[iterator + 'SearchSelectOpts'] = list.fields[fld].searchOptions; + || list.fields[fld].searchType == 'select' || list.fields[fld].searchType == 'select_or')) { + scope[iterator + 'SelectShow' + modifier] = true; + scope[iterator + 'SearchSelectOpts' + modifier] = list.fields[fld].searchOptions; } else if (list.fields[fld].searchType && list.fields[fld].searchType == 'int') { - scope[iterator + 'HideSearchType'] = true; + scope[iterator + 'HideSearchType' + modifier] = true; } else if (list.fields[fld].searchType && list.fields[fld].searchType == 'isnull') { - scope[iterator + 'SearchType'] = 'isnull'; - scope[iterator + 'InputDisable'] = true; - scope[iterator + 'SearchValue'] = 'true'; + scope[iterator + 'SearchType' + modifier] = 'isnull'; + scope[iterator + 'InputDisable' + modifier] = true; + scope[iterator + 'SearchValue' + modifier] = 'true'; } scope.search(iterator); } - scope.resetSearch = function(iterator) { + scope.resetSearch = function(iterator, widget) { // Respdond to click of reset button - setDefaults(); + setDefaults(widget); // Force removal of search keys from the URL window.location = '/#' + $location.path(); + scope.search(iterator); } - scope.setSearchType = function(iterator, type, label) { - scope[iterator + 'SearchTypeLabel'] = label; - scope[iterator + 'SearchType'] = type; - scope.search(iterator); + //scope.setSearchType = function(iterator, type, label) { + // scope[iterator + 'SearchTypeLabel'] = label; + // scope[iterator + 'SearchType'] = type; + // scope.search(iterator); + // } + + + if (scope.removeDoSearch) { + scope.removeDoSearch(); + } + scope.removeDoSearch = scope.$on('doSearch', function(e, iterator, page, load, spin) { + // + // Execute the search + // + scope[iterator + 'SearchSpin'] = (spin == undefined || spin == true) ? true : false; + scope[iterator + 'Loading'] = (load == undefined || load == true) ? true : false; + var url = defaultUrl; + + //finalize and execute the query + scope[iterator + 'Page'] = (page) ? parseInt(page) - 1 : 0; + if (/\/$/.test(url)) { + url += '?' + scope[iterator + 'SearchParams']; + } + else { + url += '&' + scope[iterator + 'SearchParams']; + } + url = url.replace(/\&\&/,'&'); + url += (scope[iterator + 'PageSize']) ? '&page_size=' + scope[iterator + 'PageSize'] : ""; + if (page) { + url += '&page=' + page; + } + if (scope[iterator + 'ExtraParms']) { + url += scope[iterator + 'ExtraParms']; + } + Refresh({ scope: scope, set: set, iterator: iterator, url: url }); + }); + + + if (scope.removePrepareSearch) { + scope.removePrepareSearch(); + } + scope.removePrepareSearch = scope.$on('prepareSearch', function(e, iterator, page, load, spin) { + // + // Start build the search key/value pairs. This will process the first search widget, if the + // selected field is an object type (used on activity stream). + // + scope[iterator + 'HoldInput'] = true; + scope[iterator + 'SearchParams'] = ''; + if (list.fields[scope[iterator + 'SearchField']].searchObject && + list.fields[scope[iterator + 'SearchField']].searchObject !== 'all') { + //This is specifically for activity stream. We need to identify which type of object is being searched + //and then provide a list of PK values corresponding to the list of objects the user is interested in. + var objs = list.fields[scope[iterator + 'SearchField']].searchObject; + var o = (objs == 'inventories') ? 'inventory' : objs.replace(/s$/,''); + scope[iterator + 'SearchParams'] = 'or__object1=' + o + '&or__object2=' + o; + if (scope[iterator + 'SearchValue']) { + var objUrl = GetBasePath('base') + objs + '/?name__icontains=' + scope[iterator + 'SearchValue']; + Rest.setUrl(objUrl); + Rest.get() + .success( function(data, status, headers, config) { + var list=''; + for (var i=0; i < data.results.length; i++) { + list += "," + data.results[i].id; + } + list = list.replace(/^\,/,''); + if (!Empty(list)) { + scope[iterator + 'SearchParams'] += '&or__object1_id__in=' + list + '&or__object2_id__in=' + list; + } + //scope[iterator + 'SearchParams'] += (sort_order) ? '&order_by=' + escape(sort_order) : ""; + scope.$emit('prepareSearch2', iterator, page, load, spin, 2); + }) + .error( function(data, status, headers, config) { + ProcessErrors(scope, data, status, null, + { hdr: 'Error!', msg: 'Retrieving list of ' + obj + ' where name contains: ' + scope[iterator + 'SearchValue'] + + ' GET returned status: ' + status }); + }); + } + else { + scope.$emit('prepareSearch2', iterator, page, load, spin, 2); + } + } + else { + scope.$emit('prepareSearch2', iterator, page, load, spin, 1); + } + }); + + if (scope.removePrepareSearch2) { + scope.removePrepareSearch2(); + } + scope.removePrepareSearch2 = scope.$on('prepareSearch2', function(e, iterator, page, load, spin, startingWidget) { + // Continue building the search by examining the remaining search widgets. If we're looking at activity_stream, + // there's more than one. + for (var i=startingWidget; i <= 3; i++) { + var modifier = (i == 1) ? '' : i; + scope[iterator + 'HoldInput' + modifier] = true; + if ( $('#search-widget-container' + modifier) ) { + // if the search widget exists, add its parameters to the query + if ( (!scope[iterator + 'SelectShow' + modifier] && !Empty(scope[iterator + 'SearchValue' + modifier])) || + (scope[iterator + 'SelectShow' + modifier] && scope[iterator + 'SearchSelectValue' + modifier]) || + (list.fields[scope[iterator + 'SearchField' + modifier]] && + list.fields[scope[iterator + 'SearchField' + modifier]].searchType == 'gtzero') ) { + if (list.fields[scope[iterator + 'SearchField' + modifier]].searchField) { + scope[iterator + 'SearchParams'] = list.fields[scope[iterator + 'SearchField' + modifier]].searchField + '__'; + } + else if (list.fields[scope[iterator + 'SearchField' + modifier]].sourceModel) { + // handle fields whose source is a related model e.g. inventories.organization + scope[iterator + 'SearchParams'] = list.fields[scope[iterator + 'SearchField' + modifier]].sourceModel + '__' + + list.fields[scope[iterator + 'SearchField' + modifier]].sourceField + '__'; + } + else if ( (list.fields[scope[iterator + 'SearchField' + modifier]].searchType == 'select') && + (scope[iterator + 'SearchSelectValue' + modifier].value == '' || + scope[iterator + 'SearchSelectValue' + modifier].value == null) ) { + scope[iterator + 'SearchParams'] = scope[iterator + 'SearchField' + modifier]; + } + else { + scope[iterator + 'SearchParams'] = scope[iterator + 'SearchField' + modifier] + '__'; + } + + if ( list.fields[scope[iterator + 'SearchField' + modifier]].searchType && + (list.fields[scope[iterator + 'SearchField' + modifier]].searchType == 'int' || + list.fields[scope[iterator + 'SearchField' + modifier]].searchType == 'boolean' ) ) { + scope[iterator + 'SearchParams'] += 'int='; + } + else if ( list.fields[scope[iterator + 'SearchField' + modifier]].searchType && + list.fields[scope[iterator + 'SearchField' + modifier]].searchType == 'gtzero' ) { + scope[iterator + 'SearchParams'] += 'gt=0'; + } + else if ( (list.fields[scope[iterator + 'SearchField' + modifier]].searchType == 'select') && + (scope[iterator + 'SearchSelectValue' + modifier].value == '' || + scope[iterator + 'SearchSelectValue' + modifier].value == null) ) { + scope[iterator + 'SearchParams'] += 'iexact='; + } + else if ( (list.fields[scope[iterator + 'SearchField' + modifier]].searchType && + (list.fields[scope[iterator + 'SearchField' + modifier]].searchType == 'or')) ) { + scope[iterator + 'SearchParams'] = ''; //start over + var val = scope[iterator + 'SearchValue' + modifier]; + for (var k=0; k < list.fields[scope[iterator + 'SearchField' + modifier]].searchFields.length; k++) { + scope[iterator + 'SearchParams'] += '&or__' + + list.fields[scope[iterator + 'SearchField' + modifier]].searchFields[k] + + '__icontains=' + escape(val); + } + scope[iterator + 'SearchParams'].replace(/^\&/,''); + } + else { + scope[iterator + 'SearchParams'] += scope[iterator + 'SearchType' + modifier] + '='; + } + + if ( list.fields[scope[iterator + 'SearchField' + modifier]].searchType && + (list.fields[scope[iterator + 'SearchField' + modifier]].searchType == 'boolean' + || list.fields[scope[iterator + 'SearchField' + modifier]].searchType == 'select') ) { + scope[iterator + 'SearchParams'] += scope[iterator + 'SearchSelectValue' + modifier].value; + } + else { + if ( (!list.fields[scope[iterator + 'SearchField' + modifier]].searchType) || + (list.fields[scope[iterator + 'SearchField' + modifier]].searchType && + list.fields[scope[iterator + 'SearchField' + modifier]].searchType !== 'or') ) { + scope[iterator + 'SearchParams'] += escape(scope[iterator + 'SearchValue' + modifier]); + } + } + } + } + } + + if ( (iterator == 'inventory' && scope.inventoryFailureFilter) || + (iterator == 'host' && scope.hostFailureFilter) ) { + //Things that bypass the search widget. Should go back and add a second widget possibly on + //inventory pages and eliminate this + scope[iterator + 'SearchParams'] += '&has_active_failures=true'; + } + + if (sort_order) { + scope[iterator + 'SearchParams'] += (scope[iterator + 'SearchParams']) ? '&' : ''; + scope[iterator + 'SearchParams'] += 'order_by=' + escape(sort_order); + } + + scope.$emit('doSearch', iterator, page, load, spin); + }); + + scope.startSearch = function(iterator) { + //Called on each keydown event for seachValue field. Using a timer + //to prevent executing a search until user is finished typing. + if (scope.searchTimer) { + $timeout.cancel(scope.searchTimer); + } + scope.searchTimer = $timeout( + function() { + scope.$emit('prepareSearch', iterator); + } + , 1000); } scope.search = function(iterator, page, load, spin) { + // Called to initiate a searh. // Page is optional. Added to accomodate back function on Job Events detail. // Spin optional -set to false if spin not desired. // Load optional -set to false if loading message not desired - - scope[iterator + 'SearchSpin'] = (spin == undefined || spin == true) ? true : false; - scope[iterator + 'Loading'] = (load == undefined || load == true) ? true : false; - scope[iterator + 'SearchParms'] = ''; - var url = defaultUrl; - - if ( (scope[iterator + 'SelectShow'] == false && scope[iterator + 'SearchValue'] != '' && scope[iterator + 'SearchValue'] != undefined) || - (scope[iterator + 'SelectShow'] && scope[iterator + 'SearchSelectValue']) || - (list.fields[scope[iterator + 'SearchField']].searchType && list.fields[scope[iterator + 'SearchField']].searchType == 'gtzero') ) { - - if (list.fields[scope[iterator + 'SearchField']].searchField) { - scope[iterator + 'SearchParams'] = list.fields[scope[iterator + 'SearchField']].searchField + '__'; - } - else if (list.fields[scope[iterator + 'SearchField']].sourceModel) { - // handle fields whose source is a related model e.g. inventories.organization - scope[iterator + 'SearchParams'] = list.fields[scope[iterator + 'SearchField']].sourceModel + '__' + - list.fields[scope[iterator + 'SearchField']].sourceField + '__'; - } - else if ( (list.fields[scope[iterator + 'SearchField']].searchType == 'select') && - (scope[iterator + 'SearchSelectValue'].value == '' || - scope[iterator + 'SearchSelectValue'].value == null) ) { - scope[iterator + 'SearchParams'] = scope[iterator + 'SearchField']; - } - else { - scope[iterator + 'SearchParams'] = scope[iterator + 'SearchField'] + '__'; - } - - if ( list.fields[scope[iterator + 'SearchField']].searchType && - (list.fields[scope[iterator + 'SearchField']].searchType == 'int' || - list.fields[scope[iterator + 'SearchField']].searchType == 'boolean' ) ) { - scope[iterator + 'SearchParams'] += 'int='; - } - else if ( list.fields[scope[iterator + 'SearchField']].searchType && - list.fields[scope[iterator + 'SearchField']].searchType == 'gtzero' ) { - scope[iterator + 'SearchParams'] += 'gt=0'; - } - else if ( (list.fields[scope[iterator + 'SearchField']].searchType == 'select') && - (scope[iterator + 'SearchSelectValue'].value == '' || - scope[iterator + 'SearchSelectValue'].value == null) ) { - scope[iterator + 'SearchParams'] += 'iexact='; - } - else if ( (list.fields[scope[iterator + 'SearchField']].searchType && - (list.fields[scope[iterator + 'SearchField']].searchType == 'or')) ) { - scope[iterator + 'SearchParams'] = ''; //start over - for (var k=0; k < list.fields[scope[iterator + 'SearchField']].searchFields.length; k++) { - scope[iterator + 'SearchParams'] += '&or__' + - list.fields[scope[iterator + 'SearchField']].searchFields[k] + - '__icontains=' + escape(scope[iterator + 'SearchValue']); - } - scope[iterator + 'SearchParams'].replace(/^\&/,''); - } - else { - scope[iterator + 'SearchParams'] += scope[iterator + 'SearchType'] + '='; - } - - if ( list.fields[scope[iterator + 'SearchField']].searchType && - (list.fields[scope[iterator + 'SearchField']].searchType == 'boolean' - || list.fields[scope[iterator + 'SearchField']].searchType == 'select') ) { - scope[iterator + 'SearchParams'] += scope[iterator + 'SearchSelectValue'].value; - } - else { - if ( (list.fields[scope[iterator + 'SearchField']].searchType && - (list.fields[scope[iterator + 'SearchField']].searchType !== 'or')) ) { - scope[iterator + 'SearchParams'] += escape(scope[iterator + 'SearchValue']); - } - } - scope[iterator + 'SearchParams'] += (sort_order) ? '&order_by=' + escape(sort_order) : ''; - } - else { - scope[iterator + 'SearchParams'] = (sort_order) ? 'order_by=' + escape(sort_order) : ""; - } - - if ( (iterator == 'inventory' && scope.inventoryFailureFilter) || - (iterator == 'host' && scope.hostFailureFilter) ) { - scope[iterator + 'SearchParams'] += '&has_active_failures=true'; - } - - scope[iterator + 'Page'] = (page) ? parseInt(page) - 1 : 0; - if (/\/$/.test(url)) { - url += '?' + scope[iterator + 'SearchParams']; - } - else { - url += '&' + scope[iterator + 'SearchParams']; - } - url = url.replace(/\&\&/,'&'); - url += (scope[iterator + 'PageSize']) ? '&page_size=' + scope[iterator + 'PageSize'] : ""; - if (page) { - url += '&page=' + page; - } - if (scope[iterator + 'ExtraParms']) { - url += scope[iterator + 'ExtraParms']; - } - Refresh({ scope: scope, set: set, iterator: iterator, url: url }); + scope.$emit('prepareSearch', iterator, page, load, spin); } + scope.sort = function(fld) { // reset sort icons back to 'icon-sort' on all columns // except the one clicked diff --git a/awx/ui/static/js/lists/Streams.js b/awx/ui/static/js/lists/Streams.js index 5fd025cd08..7ed2bf2d06 100644 --- a/awx/ui/static/js/lists/Streams.js +++ b/awx/ui/static/js/lists/Streams.js @@ -17,6 +17,8 @@ angular.module('StreamListDefinition', []) index: false, hover: true, "class": "table-condensed", + searchWidgetLabel: 'Object', + searchWidgetLabel2: 'Modified by', fields: { timestamp: { @@ -32,20 +34,16 @@ angular.module('StreamListDefinition', []) sourceModel: 'user', sourceField: 'username', awToolTip: "\{\{ userToolTip \}\}", - dataPlacement: 'top' + dataPlacement: 'top', + searchPlaceholder: 'Username', + searchWidget: 2 }, objects: { label: 'Objects', ngBindHtml: 'activity.objects', - sortField: "object1__name,object2__name", + nosort: true, searchable: false }, - object_name: { - label: 'Object name', - searchOnly: true, - searchType: 'or', - searchFields: ['object1__name', 'object2__name'] - }, description: { label: 'Description', ngBindHtml: 'activity.description', @@ -53,11 +51,68 @@ angular.module('StreamListDefinition', []) searchable: false }, system_event: { - label: 'System event?', + label: 'System', searchOnly: true, searchType: 'isnull', sourceModel: 'user', - sourceField: 'username' + sourceField: 'username', + searchWidget: 2 + }, + // The following fields exist to forces loading each type of object into the search + // dropdown + all_objects: { + label: 'All', + searchOnly: true, + searchObject: 'all', + searchPlaceholder: ' ' + }, + credential_search: { + label: 'Credential', + searchOnly: true, + searchObject: 'credentials', + searchPlaceholder: 'Credential name' + }, + group_search: { + label: 'Group', + searchOnly: true, + searchObject: 'groups', + searchPlaceholder: 'Group name' + }, + host_search: { + label: 'Host', + searchOnly: true, + searchObject: 'hosts', + searchPlaceholder: 'Host name' + }, + inventory_search: { + label: 'Inventory', + searchOnly: true, + searchObject: 'inventories', + searchPlaceholder: 'Inventory name' + }, + job_template_search: { + label: 'Job Template', + searchOnly: true, + searchObject: 'job_templates', + searchPlaceholder: 'Job template name' + }, + organization_search: { + label: 'Organization', + searchOnly: true, + searchObject: 'organizations', + searchPlaceholder: 'Organization name' + }, + project_search: { + label: 'Project', + searchOnly: true, + searchObject: 'projects', + searchPlaceholder: 'Project name' + }, + user_search: { + label: 'User', + searchOnly: true, + searchObject: 'users', + searchPlaceholder: 'Username' } }, diff --git a/awx/ui/static/js/widgets/Stream.js b/awx/ui/static/js/widgets/Stream.js index 498fef4a3b..32bd4f33ed 100644 --- a/awx/ui/static/js/widgets/Stream.js +++ b/awx/ui/static/js/widgets/Stream.js @@ -174,9 +174,10 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti }]) .factory('Stream', ['$rootScope', '$location', 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait', 'StreamList', 'SearchInit', - 'PaginateInit', 'GenerateList', 'FormatDate', 'ShowStream', 'HideStream', 'BuildDescription', 'FixUrl', 'ShowDetail', + 'PaginateInit', 'GenerateList', 'FormatDate', 'ShowStream', 'HideStream', 'BuildDescription', 'FixUrl', 'BuildUrl', + 'ShowDetail', function($rootScope, $location, Rest, GetBasePath, ProcessErrors, Wait, StreamList, SearchInit, PaginateInit, GenerateList, - FormatDate, ShowStream, HideStream, BuildDescription, FixUrl, ShowDetail) { + FormatDate, ShowStream, HideStream, BuildDescription, FixUrl, BuildUrl, ShowDetail) { return function(params) { var list = StreamList; @@ -188,7 +189,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti var type = (base == 'inventories') ? 'inventory' : base.replace(/s$/,''); defaultUrl += '?or__object1=' + type + '&or__object2=' + type; } - + // Push the current page onto browser histor. If user clicks back button, restore current page without // stream widget // window.history.pushState({}, "AnsibleWorks AWX", $location.path()); @@ -203,7 +204,8 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti mode: 'edit', id: 'stream-content', breadCrumbs: true, - searchSize: 'col-lg-4' + searchSize: 'col-lg-3', + secondWidget: true }); scope.closeStream = function() { @@ -241,7 +243,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti var deleted = /^\_delete/; if (scope['activities'][i].summary_fields.object1) { if ( !deleted.test(scope['activities'][i].summary_fields.object1.name) ) { - href = FixUrl(scope['activities'][i].related.object1); + href = BuildUrl(scope['activities'][i].summary_fields.object1); scope['activities'][i].objects = "" + scope['activities'][i].summary_fields.object1.name + ""; } else { @@ -250,7 +252,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti } if (scope['activities'][i].summary_fields.object2) { if ( !deleted.test(scope['activities'][i].summary_fields.object2.name) ) { - href = FixUrl(scope['activities'][i].related.object2); + href = BuildUrl(scope['activities'][i].summary_fields.object2); scope['activities'][i].objects += ", " + scope['activities'][i].summary_fields.object2.name + ""; } else { @@ -264,7 +266,17 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti // Initialize search and paginate pieces and load data SearchInit({ scope: scope, set: list.name, list: list, url: defaultUrl }); PaginateInit({ scope: scope, list: list, url: defaultUrl }); - scope.search(list.iterator); + scope.search(list.iterator); + + /* + scope.$watch(list.iterator + 'SearchField', function(newVal, oldVal) { + console.log('newVal: ' + newVal); + html += "" + html += "\n"; + });*/ + } }]); \ No newline at end of file diff --git a/awx/ui/static/lib/ansible/directives.js b/awx/ui/static/lib/ansible/directives.js index a41558c42e..89c362dfe1 100644 --- a/awx/ui/static/lib/ansible/directives.js +++ b/awx/ui/static/lib/ansible/directives.js @@ -151,6 +151,20 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Hos } } }) + + // awPlaceholder: Dynamic placeholder set to a scope variable you want watched. + // Value will be place in field placeholder attribute. + .directive('awPlaceholder', [ function() { + return { + require: 'ngModel', + link: function(scope, elm, attrs, ctrl) { + $(elm).attr('placeholder', scope[attrs.awPlaceholder]); + scope.$watch(attrs.awPlaceholder, function(newVal, oldVal) { + $(elm).attr('placeholder',newVal); + }); + } + } + }]) // lookup Validate lookup value against API // diff --git a/awx/ui/static/lib/ansible/generator-helpers.js b/awx/ui/static/lib/ansible/generator-helpers.js index 1c46a9f2f8..0b61efb61c 100644 --- a/awx/ui/static/lib/ansible/generator-helpers.js +++ b/awx/ui/static/lib/ansible/generator-helpers.js @@ -477,20 +477,20 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers']) var iterator = params.iterator; var form = params.template; var useMini = params.mini; - var label = (params.label) ? params.label : null; + //var label = (params.label) ? params.label : null; var html= ''; + var secondWidget = params.secondWidget; html += "

\n"; html += "
\n"; - html += (label) ? "" : ""; + html += "\" id=\"search-widget-container\">\n"; + html += (form.searchWidgetLabel) ? "" : ""; html += "
\n"; html += "
\n"; - // Use standard button on mobile html += "\n"; - - // Use link and hover activation on desktop - //html += ""; - //html += "\n"; - //html += "\n"; - //html += "\n"; - + html += "
    \n"; for ( var fld in form.fields) { - if (form.fields[fld].searchable == undefined || form.fields[fld].searchable == true) { - html += "
  • " + - form.fields[fld].searchLabel + "
  • \n"; - } - else { - html += form.fields[fld].label.replace(/\/g,' ') + "')\">" + - form.fields[fld].label.replace(/\/g,' ') + "\n"; - } - } + if ( (form.fields[fld].searchable == undefined || form.fields[fld].searchable == true) + && (form.fields[fld].searchWidget == undefined || form.fields[fld].searchWidget == 1) ) { + html += "
  • " + + form.fields[fld].searchLabel + "
  • \n"; + } + else { + html += form.fields[fld].label.replace(/\/g,' ') + "')\">" + + form.fields[fld].label.replace(/\/g,' ') + "\n"; + } + } } html += "
\n"; html += "
\n"; @@ -529,37 +524,71 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers']) html += "\">\n"; html += "\n"; - - /* - html += "
\n"; - html += "\n"; - html += "\n"; - html += "
\n"; - */ - + html += "\" ng-model=\"" + iterator + "SearchValue\" ng-keydown=\"startSearch('" + iterator + "')\" " + + "aw-placeholder=\"" + iterator + "SearchPlaceholder\" type=\"text\" ng-disabled=\"" + iterator + "InputDisable || " + iterator + + "HoldInput\">\n"; // Reset button html += "
\n"; - html += "\n"; html += "
\n"; html += "
\n"; - html += "
\n"; + + // Search Widget 2 + // Used on activity stream. Set 'searchWidget2: true' on fields to be included. + if (secondWidget) { + html += "
\n"; + html += (form.searchWidgetLabel2) ? "" : ""; + html += "
\n"; + html += "
\n"; + html += "\n"; + + html += "
    \n"; + for ( var fld in form.fields) { + if ( (form.fields[fld].searchable == undefined || form.fields[fld].searchable == true) + && form.fields[fld].searchWidget == 2 ) { + html += "
  • " + + form.fields[fld].searchLabel + "
  • \n"; + } + else { + html += form.fields[fld].label.replace(/\/g,' ') + "', 2)\">" + + form.fields[fld].label.replace(/\/g,' ') + "\n"; + } + } + } + html += "
\n"; + html += "
\n"; + + html += "\n"; + + // Reset button + html += "
\n"; + html += "\n"; + html += "
\n"; + html += "
\n"; + html += "
\n"; + } + + // Spinner html += "
\n"; + return html; } diff --git a/awx/ui/static/lib/ansible/list-generator.js b/awx/ui/static/lib/ansible/list-generator.js index 6ee4d26908..b0d2fcd659 100644 --- a/awx/ui/static/lib/ansible/list-generator.js +++ b/awx/ui/static/lib/ansible/list-generator.js @@ -194,7 +194,8 @@ angular.module('ListGenerator', ['GeneratorHelpers']) */ if (options.searchSize) { - html += SearchWidget({ iterator: list.iterator, template: list, mini: true , size: options.searchSize }); + html += SearchWidget({ iterator: list.iterator, template: list, mini: true , size: options.searchSize, + secondWidget: options.secondWidget }); } else if (options.mode == 'summary') { html += SearchWidget({ iterator: list.iterator, template: list, mini: true , size: 'col-lg-6' }); @@ -214,6 +215,7 @@ angular.module('ListGenerator', ['GeneratorHelpers']) if (options.searchSize) { // User supplied searchSize, calc the remaining var size = parseInt(options.searchSize.replace(/([A-Z]|[a-z]|\-)/g,'')); + size += (options.secondWidget) ? 3 : 0; html += 'col-lg-' + (11 - size); } else if (options.mode == 'summary') { From 6764b5857aa088f921960e78985b732b7be5c9fe Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 18 Nov 2013 09:21:40 -0500 Subject: [PATCH 38/38] Move auditlog migration to 0028 for main branch merge --- awx/main/migrations/0028_v14_changes.py | 425 ++++++++++++++++++++++++ 1 file changed, 425 insertions(+) create mode 100644 awx/main/migrations/0028_v14_changes.py diff --git a/awx/main/migrations/0028_v14_changes.py b/awx/main/migrations/0028_v14_changes.py new file mode 100644 index 0000000000..4b07d4f055 --- /dev/null +++ b/awx/main/migrations/0028_v14_changes.py @@ -0,0 +1,425 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'ActivityStream' + db.create_table(u'main_activitystream', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='activity_stream', null=True, on_delete=models.SET_NULL, to=orm['auth.User'])), + ('operation', self.gf('django.db.models.fields.CharField')(max_length=13)), + ('timestamp', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('changes', self.gf('django.db.models.fields.TextField')(blank=True)), + ('object1_id', self.gf('django.db.models.fields.PositiveIntegerField')(db_index=True)), + ('object1', self.gf('django.db.models.fields.TextField')()), + ('object1_type', self.gf('django.db.models.fields.TextField')()), + ('object2_id', self.gf('django.db.models.fields.PositiveIntegerField')(null=True, db_index=True)), + ('object2', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + ('object2_type', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + ('object_relationship_type', self.gf('django.db.models.fields.TextField')(blank=True)), + )) + db.send_create_signal('main', ['ActivityStream']) + + + def backwards(self, orm): + # Deleting model 'ActivityStream' + db.delete_table(u'main_activitystream') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'main.activitystream': { + 'Meta': {'object_name': 'ActivityStream'}, + 'changes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object1': ('django.db.models.fields.TextField', [], {}), + 'object1_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'object1_type': ('django.db.models.fields.TextField', [], {}), + 'object2': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'object2_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'db_index': 'True'}), + 'object2_type': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'object_relationship_type': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'operation': ('django.db.models.fields.CharField', [], {'max_length': '13'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_stream'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}) + }, + 'main.authtoken': { + 'Meta': {'object_name': 'AuthToken'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'request_hash': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '40', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_tokens'", 'to': u"orm['auth.User']"}) + }, + 'main.credential': { + 'Meta': {'unique_together': "[('user', 'team', 'kind', 'name')]", 'object_name': 'Credential'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cloud': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'credential\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'kind': ('django.db.models.fields.CharField', [], {'default': "'ssh'", 'max_length': '32'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'credential\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'ssh_key_data': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'ssh_key_unlock': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'sudo_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'sudo_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'team': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'credentials'", 'null': 'True', 'blank': 'True', 'to': "orm['main.Team']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'credentials'", 'null': 'True', 'blank': 'True', 'to': u"orm['auth.User']"}), + 'username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}) + }, + 'main.group': { + 'Meta': {'unique_together': "(('name', 'inventory'),)", 'object_name': 'Group'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'group\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'groups_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_inventory_sources': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'groups'", 'blank': 'True', 'to': "orm['main.Host']"}), + 'hosts_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'groups'", 'to': "orm['main.Inventory']"}), + 'inventory_sources': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'groups'", 'blank': 'True', 'to': "orm['main.InventorySource']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'group\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'parents': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'children'", 'blank': 'True', 'to': "orm['main.Group']"}), + 'total_groups': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'total_hosts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}) + }, + 'main.host': { + 'Meta': {'unique_together': "(('name', 'inventory'),)", 'object_name': 'Host'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'host\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_inventory_sources': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instance_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts'", 'to': "orm['main.Inventory']"}), + 'inventory_sources': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'hosts'", 'blank': 'True', 'to': "orm['main.InventorySource']"}), + 'last_job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts_as_last_job+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Job']", 'blank': 'True', 'null': 'True'}), + 'last_job_host_summary': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts_as_last_job_summary+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.JobHostSummary']", 'blank': 'True', 'null': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'host\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}) + }, + 'main.inventory': { + 'Meta': {'unique_together': "[('name', 'organization')]", 'object_name': 'Inventory'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventory\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'groups_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_inventory_sources': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'hosts_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory_sources_with_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventory\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventories'", 'to': "orm['main.Organization']"}), + 'total_groups': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'total_hosts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'total_inventory_sources': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}) + }, + 'main.inventorysource': { + 'Meta': {'object_name': 'InventorySource'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventorysource\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'inventory_sources'", 'null': 'True', 'blank': 'True', 'to': "orm['main.Credential']"}), + 'current_update': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'inventory_source_as_current_update+'", 'null': 'True', 'to': "orm['main.InventoryUpdate']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'group': ('awx.main.fields.AutoOneToOneField', [], {'related_name': "'inventory_source'", 'null': 'True', 'default': 'None', 'to': "orm['main.Group']", 'blank': 'True', 'unique': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'inventory_sources'", 'null': 'True', 'to': "orm['main.Inventory']"}), + 'last_update': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'inventory_source_as_last_update+'", 'null': 'True', 'to': "orm['main.InventoryUpdate']"}), + 'last_update_failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventorysource\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'overwrite': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'overwrite_vars': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'source': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + 'source_path': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'source_regions': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'source_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'none'", 'max_length': '32', 'null': 'True'}), + 'update_interval': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'update_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'main.inventoryupdate': { + 'Meta': {'object_name': 'InventoryUpdate'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cancel_flag': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'celery_task_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventoryupdate\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory_source': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventory_updates'", 'to': "orm['main.InventorySource']"}), + 'job_args': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'job_cwd': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_env': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + 'license_error': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventoryupdate\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'result_stdout': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'result_traceback': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'new'", 'max_length': '20'}) + }, + 'main.job': { + 'Meta': {'object_name': 'Job'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cancel_flag': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'celery_task_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}), + 'cloud_credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs_as_cloud_credential+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'job\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Credential']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'jobs'", 'blank': 'True', 'through': "orm['main.JobHostSummary']", 'to': "orm['main.Host']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'job_args': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'job_cwd': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_env': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + 'job_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.JobTemplate']", 'blank': 'True', 'null': 'True'}), + 'job_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'launch_type': ('django.db.models.fields.CharField', [], {'default': "'manual'", 'max_length': '20'}), + 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'job\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'playbook': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Project']"}), + 'result_stdout': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'result_traceback': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'new'", 'max_length': '20'}), + 'verbosity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}) + }, + 'main.jobevent': { + 'Meta': {'ordering': "('pk',)", 'object_name': 'JobEvent'}, + 'changed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now_add': 'True', 'blank': 'True'}), + 'event': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'event_data': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'host': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_events_as_primary_host'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Host']", 'blank': 'True', 'null': 'True'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'job_events'", 'blank': 'True', 'to': "orm['main.Host']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_events'", 'to': "orm['main.Job']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'children'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.JobEvent']", 'blank': 'True', 'null': 'True'}), + 'play': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'task': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}) + }, + 'main.jobhostsummary': { + 'Meta': {'ordering': "('-pk',)", 'unique_together': "[('job', 'host')]", 'object_name': 'JobHostSummary'}, + 'changed': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now_add': 'True', 'blank': 'True'}), + 'dark': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'host': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_host_summaries'", 'to': "orm['main.Host']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_host_summaries'", 'to': "orm['main.Job']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'ok': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'processed': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'skipped': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}) + }, + 'main.jobtemplate': { + 'Meta': {'object_name': 'JobTemplate'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cloud_credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates_as_cloud_credential+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'jobtemplate\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'host_config_key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'job_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'jobtemplate\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'playbook': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Project']"}), + 'verbosity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}) + }, + 'main.organization': { + 'Meta': {'object_name': 'Organization'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'admins': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'admin_of_organizations'", 'blank': 'True', 'to': u"orm['auth.User']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'organization\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'organization\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': "orm['main.Project']"}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': u"orm['auth.User']"}) + }, + 'main.permission': { + 'Meta': {'object_name': 'Permission'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'permission\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'permission\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Project']"}), + 'team': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Team']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}) + }, + 'main.profile': { + 'Meta': {'object_name': 'Profile'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ldap_dn': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('awx.main.fields.AutoOneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': u"orm['auth.User']"}) + }, + 'main.project': { + 'Meta': {'object_name': 'Project'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'project\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'projects'", 'null': 'True', 'blank': 'True', 'to': "orm['main.Credential']"}), + 'current_update': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'project_as_current_update+'", 'null': 'True', 'to': "orm['main.ProjectUpdate']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_update': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'project_as_last_update+'", 'null': 'True', 'to': "orm['main.ProjectUpdate']"}), + 'last_update_failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'local_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'project\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'scm_branch': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'null': 'True', 'blank': 'True'}), + 'scm_clean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_delete_on_next_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_delete_on_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '8', 'null': 'True', 'blank': 'True'}), + 'scm_update_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'null': 'True', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'ok'", 'max_length': '32', 'null': 'True'}) + }, + 'main.projectupdate': { + 'Meta': {'object_name': 'ProjectUpdate'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cancel_flag': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'celery_task_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'projectupdate\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'job_args': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'job_cwd': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_env': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'projectupdate\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'project_updates'", 'to': "orm['main.Project']"}), + 'result_stdout': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'result_traceback': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'new'", 'max_length': '20'}) + }, + 'main.team': { + 'Meta': {'unique_together': "[('organization', 'name')]", 'object_name': 'Team'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'team\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'team\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'teams'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Organization']"}), + 'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': "orm['main.Project']"}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': u"orm['auth.User']"}) + }, + u'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'}) + }, + u'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_tagged_items'", 'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_items'", 'to': u"orm['taggit.Tag']"}) + } + } + + complete_apps = ['main']