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 2c0fc96d43..6f15198778 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') @@ -1032,6 +1035,58 @@ class JobEventSerializer(BaseSerializer): pass return d +class ActivityStreamSerializer(BaseSerializer): + + class Meta: + model = ActivityStream + fields = ('id', 'url', 'related', 'summary_fields', 'timestamp', 'operation', 'changes', + 'object1_id', 'object1', 'object1_type', 'object2_id', 'object2', 'object2_type', 'object_relationship_type') + + def get_related(self, obj): + if obj is None: + return {} + 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['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['object2'] = 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: + 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) + ")") + 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: + 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.object2_type + ".objects.get(id=" + str(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: + pass + return d class AuthTokenSerializer(serializers.Serializer): diff --git a/awx/api/urls.py b/awx/api/urls.py index e868610231..71dc9531e0 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/api/views.py b/awx/api/views.py index 9e7951ecb3..79ae6fe382 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): @@ -1055,6 +1056,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/middleware.py b/awx/main/middleware.py new file mode 100644 index 0000000000..87959af432 --- /dev/null +++ b/awx/main/middleware.py @@ -0,0 +1,50 @@ +from django.conf import settings +from django.contrib.auth.models import User +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): + + 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: + user = None + + self.instances = [] + set_actor = curry(self.set_actor, user) + 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): + post_save.disconnect(dispatch_uid=self.disp_uid) + self.finished = True + if self.isActivityStreamEvent: + 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", "Job") and instance.id is not None: + instance.delete() + return response + + def set_actor(self, user, sender, instance, **kwargs): + 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 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'] diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index abb68b57da..63cc7ddafa 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -6,6 +6,8 @@ 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 # the dumpdata command; see https://github.com/alex/django-taggit/issues/155). @@ -27,3 +29,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/activity_stream.py b/awx/main/models/activity_stream.py new file mode 100644 index 0000000000..76333bd564 --- /dev/null +++ b/awx/main/models/activity_stream.py @@ -0,0 +1,41 @@ +# Copyright (c) 2013 AnsibleWorks, Inc. +# All Rights Reserved. + + +from django.db import models +from django.core.urlresolvers import reverse +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")), + ('delete', _("Entity Deleted")), + ('associate', _("Entity Associated 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=13, choices=OPERATION_CHOICES) + timestamp = models.DateTimeField(auto_now_add=True) + 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) + + def get_absolute_url(self): + return reverse('api:activity_stream_detail', args=(self.pk,)) diff --git a/awx/main/models/base.py b/awx/main/models/base.py index 8304144f50..8c36e1516f 100644 --- a/awx/main/models/base.py +++ b/awx/main/models/base.py @@ -370,3 +370,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=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/registrar.py b/awx/main/registrar.py new file mode 100644 index 0000000000..c339c17116 --- /dev/null +++ b/awx/main/registrar.py @@ -0,0 +1,42 @@ +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): + 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=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: + 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: + 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: + 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 = getattr(model, m2mfield.name) + m2m_changed.disconnect(dispatch_uid=str(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..44fa6447b2 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 @@ -11,6 +12,7 @@ from django.dispatch import receiver # AWX from awx.main.models import * +from awx.main.utils import model_instance_diff, model_to_dict, camelcase_to_underscore __all__ = [] @@ -168,3 +170,65 @@ 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=camelcase_to_underscore(instance.__class__.__name__), + 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): + try: + old = sender.objects.get(id=instance.id) + except sender.DoesNotExist: + return + + new = instance + changes = model_instance_diff(old, new) + 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() + + +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() + +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 = 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=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() 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") diff --git a/awx/main/utils.py b/awx/main/utils.py index 562884a346..7665508b27 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,45 @@ 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 + +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 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 = ( 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/Credentials.js b/awx/ui/static/js/controllers/Credentials.js index 87d9449899..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,13 +112,13 @@ 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, 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 +161,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 +177,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 +243,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, @@ -358,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/Home.js b/awx/ui/static/js/controllers/Home.js index 735604c501..f3bb8ac9bc 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,15 @@ 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[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']) { scope[HomeHostList.iterator + 'InputDisable'] = true; scope[HomeHostList.iterator + 'SearchValue'] = $routeParams['has_active_failures']; 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 0484e450cc..287049fe85 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. @@ -539,6 +546,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 @@ -573,16 +581,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); } }; @@ -640,5 +651,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 f325f8161f..743311cc8f 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'); } @@ -47,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 }); @@ -70,12 +75,12 @@ 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, 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. @@ -92,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); @@ -107,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 }); }); @@ -121,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. @@ -184,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]; @@ -191,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 }); }); @@ -226,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 }); @@ -250,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/ActivityDetail.js b/awx/ui/static/js/forms/ActivityDetail.js new file mode 100644 index 0000000000..b5cadc53d3 --- /dev/null +++ b/awx/ui/static/js/forms/ActivityDetail.js @@ -0,0 +1,64 @@ +/********************************************* + * 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 + }, + id: { + label: 'Event ID', + 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/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/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' 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 048e0bc8c7..9352ef1dc7 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 25bcb7c524..0f476db130 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/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/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 d9ac0443cf..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,211 +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' + 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 { - 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 == undefined || - // list.fields[scope[iterator + 'SearchField']].searchType == 'gtzero' ) { - 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/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 ff17ad1cb4..7ed2bf2d06 100644 --- a/awx/ui/static/js/lists/Streams.js +++ b/awx/ui/static/js/lists/Streams.js @@ -17,22 +17,102 @@ angular.module('StreamListDefinition', []) index: false, hover: true, "class": "table-condensed", + searchWidgetLabel: 'Object', + searchWidgetLabel2: 'Modified by', fields: { - event_time: { + timestamp: { + label: 'Event Time', key: true, - label: 'When' + desc: true, + noLink: true, + searchable: false }, user: { - label: 'Who', + label: 'User', + ngBindHtml: 'activity.user', sourceModel: 'user', - sourceField: 'username' + sourceField: 'username', + awToolTip: "\{\{ userToolTip \}\}", + dataPlacement: 'top', + searchPlaceholder: 'Username', + searchWidget: 2 }, - operation: { - label: 'Operation' + objects: { + label: 'Objects', + ngBindHtml: 'activity.objects', + nosort: true, + searchable: false }, description: { - label: 'Description' + label: 'Description', + ngBindHtml: 'activity.description', + nosort: true, + searchable: false + }, + system_event: { + label: 'System', + searchOnly: true, + searchType: 'isnull', + sourceModel: 'user', + 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' } }, @@ -58,5 +138,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 ee90e98645..32bd4f33ed 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,15 +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', 'BuildUrl', + 'ShowDetail', function($rootScope, $location, Rest, GetBasePath, ProcessErrors, Wait, StreamList, SearchInit, PaginateInit, GenerateList, - FormatDate, ShowStream, HideStream) { + FormatDate, ShowStream, HideStream, BuildDescription, FixUrl, BuildUrl, ShowDetail) { return function(params) { var list = StreamList; - var defaultUrl = $basePath + 'html/event_log.html/'; + 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 @@ -70,23 +196,29 @@ 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, { mode: 'edit', id: 'stream-content', breadCrumbs: true, - searchSize: 'col-lg-4' + searchSize: 'col-lg-3', + secondWidget: true }); scope.closeStream = function() { - HideStream(); - } + HideStream(); + } scope.refreshStream = function() { - scope['activities'].splice(10,10); - //scope.search(list.iterator); - } + scope.search(list.iterator); + } + + scope.showDetail = function(id) { + ShowDetail(id); + } if (scope.removePostRefresh) { scope.removePostRefresh(); @@ -94,12 +226,41 @@ 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') { + // 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) { + if ( !deleted.test(scope['activities'][i].summary_fields.object1.name) ) { + href = BuildUrl(scope['activities'][i].summary_fields.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) { + if ( !deleted.test(scope['activities'][i].summary_fields.object2.name) ) { + href = BuildUrl(scope['activities'][i].summary_fields.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(); }); // Initialize search and paginate pieces and load data @@ -107,6 +268,15 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti PaginateInit({ scope: scope, list: list, url: defaultUrl }); 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 4cff7f0b4e..0b61efb61c 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 += "\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"; @@ -528,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 1891c4a53a..39ddf3a8ce 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') { diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index c067f5b9f7..b6ac3d47fd 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -75,6 +75,7 @@ +