From d4426d88c46db1f92dd617e3cc2541e2cc4718a5 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 27 Jan 2014 11:37:18 -0500 Subject: [PATCH 01/32] Initial work towards the celery refactor... adjusting logic to allow building a worker chain... temporarily relax requirements on status checks --- awx/main/models/base.py | 10 +++- awx/main/models/inventory.py | 6 ++ awx/main/models/jobs.py | 51 ++++++++++++++++ awx/main/models/projects.py | 6 ++ awx/main/tasks.py | 111 +++++------------------------------ awx/settings/defaults.py | 1 + 6 files changed, 87 insertions(+), 98 deletions(-) diff --git a/awx/main/models/base.py b/awx/main/models/base.py index e6653881cf..0182cee98d 100644 --- a/awx/main/models/base.py +++ b/awx/main/models/base.py @@ -366,7 +366,7 @@ class CommonTask(PrimordialModel): def _get_passwords_needed_to_start(self): return [] - def start(self, **kwargs): + def start_signature(self, **kwargs): task_class = self._get_task_class() if not self.can_start: return False @@ -377,7 +377,13 @@ class CommonTask(PrimordialModel): self.status = 'pending' self.save(update_fields=['status']) transaction.commit() - task_result = task_class().delay(self.pk, **opts) + task_actual = task_class().si(self.pk, **opts) + return task_actual + + def start(self, **kwargs): + task_actual = self.start_signature(**kwargs) + # TODO: Callback for status + task_result = task_actual.delay() # Reload instance from database so we don't clobber results from task # (mainly from tests when using Django 1.4.x). instance = self.__class__.objects.get(pk=self.pk) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index dfadfd8bd4..6867fe251d 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -685,6 +685,12 @@ class InventorySource(PrimordialModel): # FIXME: Prevent update when another one is active! return bool(self.source) + def update_signature(self, **kwargs): + if self.can_update: + inventory_update = self.inventory_updates.create() + inventory_update_sig = inventory_update.start_signature() + return (inventory_update, inventory_update_sig) + def update(self, **kwargs): if self.can_update: inventory_update = self.inventory_updates.create() diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index d22099c676..02a5c77cb5 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -18,6 +18,7 @@ import yaml # Django from django.conf import settings from django.db import models +from django.db import transaction from django.utils.translation import ugettext_lazy as _ from django.core.exceptions import ValidationError, NON_FIELD_ERRORS from django.core.urlresolvers import reverse @@ -30,6 +31,11 @@ from jsonfield import JSONField # AWX from awx.main.models.base import * +# Celery +from celery import chain + +logger = logging.getLogger('awx.main.models.jobs') + __all__ = ['JobTemplate', 'Job', 'JobHostSummary', 'JobEvent'] @@ -328,6 +334,51 @@ class Job(CommonTask): def processed_hosts(self): return self._get_hosts(job_host_summaries__processed__gt=0) + def start(self, **kwargs): + task_class = self._get_task_class() + if not self.can_start: + return False + needed = self._get_passwords_needed_to_start() + opts = dict([(field, kwargs.get(field, '')) for field in needed]) + if not all(opts.values()): + return False + self.status = 'waiting' + self.save(update_fields=['status']) + transaction.commit() + + runnable_tasks = [] + inventory_updates_actual = [] + project_update_actual = None + + project = self.project + inventory = self.inventory + is_qs = inventory.inventory_sources.filter(active=True, update_on_launch=True) + if project.scm_update_on_launch: + # TODO: We assume these return a tuple but not on error + project_update, project_update_sig = project.update_signature() + if not project_update: + # TODO: Set error here + pass + else: + project_update_actual = project_update + # TODO: append a callback to gather the status? + runnable_tasks.append(project_update_sig) + # TODO: need to add celery task id to proj update instance + if is_qs.count(): + for inventory_source in is_qs: + # TODO: We assume these return a tuple but not on error + inventory_update, inventory_update_sig = inventory_source.update_signature() + if not inventory_update: + # TODO: Set error here + pass + else: + inventory_updates_actual.append(inventory_update) + runnable_tasks.append(inventory_update_sig) + job_actual = task_class().si(self.pk, **opts) + runnable_tasks.append(job_actual) + print runnable_tasks + res = chain(runnable_tasks)() + return True class JobHostSummary(BaseModel): ''' diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 7a531a9d10..9738d2627f 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -282,6 +282,12 @@ class Project(CommonModel): # FIXME: Prevent update when another one is active! return bool(self.scm_type)# and not self.current_update) + def update_signature(self, **kwargs): + if self.can_update: + project_update = self.project_updates.create() + project_update_sig = project_update.start_signature() + return (project_update, project_updaate_sig) + def update(self, **kwargs): if self.can_update: project_update = self.project_updates.create() diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 7b2702cea3..2b31fe8151 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -108,6 +108,7 @@ class BaseTask(Task): 'yes': 'yes', 'no': 'no', '': '', + } def build_env(self, instance, **kwargs): @@ -208,10 +209,12 @@ class BaseTask(Task): # we have a way to know the task is still running, otherwise the # post_run_hook below would cancel long-running tasks that are # really still active). - instance = self.update_model(instance.pk, status='running') - if instance.cancel_flag: - child.close(True) - canceled = True + #TODO: Find replacement for cancel flag + #TODO: Something about checking celery status + # instance = self.update_model(instance.pk, status='running') + # if instance.cancel_flag: + # child.close(True) + # canceled = True # FIXME: Find a way to determine if task is hung waiting at a prompt. if idle_timeout and (time.time() - last_stdout_update) > idle_timeout: child.close(True) @@ -444,98 +447,14 @@ class RunJob(BaseTask): ''' Hook for checking job before running. ''' - project_update = None - inventory_updates = None - while True: - pk = job.pk - if job.status in ('pending', 'waiting'): - project = job.project - pu_qs = project.project_updates.filter(status__in=('pending', 'running')) - inventory = job.inventory - base_iu_qs = InventoryUpdate.objects.filter(inventory_source__inventory=inventory) - iu_qs = base_iu_qs.filter(status__in=('pending', 'running')) - is_qs = inventory.inventory_sources.filter(active=True, update_on_launch=True) - # Refresh the current project_update instance (if set). - if project_update: - try: - project_update = project.project_updates.filter(pk=project_update.pk)[0] - except IndexError: - msg = 'Unable to check project update.' - job = self.update_model(pk, status='error', - result_traceback=msg) - return False - # Refresh the current inventory_update instance(s) (if set). - if inventory_updates: - inventory_update_pks = [x.pk for x in inventory_updates] - inventory_updates = list(base_iu_qs.filter(pk__in=inventory_update_pks)) - - # If the job needs to update the project first (and there is no - # specific project update defined). - if not project_update and project.scm_update_on_launch: - job = self.update_model(pk, status='waiting') - try: - project_update = pu_qs[0] - except IndexError: - project_update = project.update() - if not project_update: - msg = 'Unable to update project before launch.' - job = self.update_model(pk, status='error', - result_traceback=msg) - return False - #print 'job %d waiting on project update %d' % (pk, project_update.pk) - time.sleep(2.0) - # If the job needs to update any inventory first (and there are - # no current inventory updates pending). - elif inventory_updates is None and is_qs.count(): - job = self.update_model(pk, status='waiting') - inventory_updates = [] - msgs = [] - for inventory_source in is_qs: - try: - inventory_update = iu_qs.filter(inventory_source=inventory_source)[0] - except IndexError: - inventory_update = inventory_source.update() - if not inventory_update: - msgs.append('Unable to update inventory source %d before launch' % inventory_source.pk) - continue - inventory_updates.append(inventory_update) - if msgs: - msg = '\n'.join(msgs) - job = self.update_model(pk, status='error', - result_traceback=msg) - return False - time.sleep(2.0) - # If project update has failed, abort the job. - elif project_update and project_update.failed: - msg = 'Project update %d failed with status = %s.' % (project_update.pk, project_update.status) - job = self.update_model(pk, status='error', - result_traceback=msg) - return False - # If any inventory update has failed, abort the job. - elif inventory_updates and any([x.failed for x in inventory_updates]): - msgs = [] - for inventory_update in inventory_updates: - if inventory_update.failed: - msgs.append('Inventory update %d failed with status = %s.' % (inventory_update.pk, inventory_update.status)) - if msgs: - msg = '\n'.join(msgs) - job = self.update_model(pk, status='error', - result_traceback=msg) - return False - # Check if blocked by any other active project or inventory updates. - elif pu_qs.count() or iu_qs.count(): - #print 'job %d waiting on' % pk, pu_qs - job = self.update_model(pk, status='waiting') - time.sleep(4.0) - # Otherwise continue running the job. - else: - job = self.update_model(pk, status='pending') - return True - elif job.cancel_flag: - job = self.update_model(pk, status='canceled') - return False - else: - return False + if job.status in ('pending', 'waiting'): + job = self.update_model(job.pk, status='pending') + return True + elif job.cancel_flag: + job = self.update_model(job.pk, status='canceled') + return False + else: + return False def post_run_hook(self, job, **kwargs): ''' diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 1498d9ef27..fca79f1480 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -282,6 +282,7 @@ CELERYD_TASK_TIME_LIMIT = None CELERYD_TASK_SOFT_TIME_LIMIT = None CELERYBEAT_SCHEDULER = 'djcelery.schedulers.DatabaseScheduler' CELERYBEAT_MAX_LOOP_INTERVAL = 60 +CELERY_RESULT_BACKEND = 'djcelery.backends.database:DatabaseBackend' # Any ANSIBLE_* settings will be passed to the subprocess environment by the # celery task. From 6f069f6ff06be42216fd0f60d3a71b9c5fbeac3b Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 27 Jan 2014 12:20:21 -0500 Subject: [PATCH 02/32] Leave another TODO on the job runner, fix a misspelling on the project update hook --- awx/main/models/jobs.py | 1 + awx/main/models/projects.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 02a5c77cb5..d0ac720b43 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -342,6 +342,7 @@ class Job(CommonTask): opts = dict([(field, kwargs.get(field, '')) for field in needed]) if not all(opts.values()): return False + # TODO: This is temporary to allow a dependent task to continue self.status = 'waiting' self.save(update_fields=['status']) transaction.commit() diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 9738d2627f..4f36f00405 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -286,7 +286,7 @@ class Project(CommonModel): if self.can_update: project_update = self.project_updates.create() project_update_sig = project_update.start_signature() - return (project_update, project_updaate_sig) + return (project_update, project_update_sig) def update(self, **kwargs): if self.can_update: From 4a598d7c0ab9a296be95ef20cc9eeb0795f9d4d7 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 27 Jan 2014 11:37:18 -0500 Subject: [PATCH 03/32] Initial work towards the celery refactor... adjusting logic to allow building a worker chain... temporarily relax requirements on status checks --- awx/main/models/base.py | 10 +++- awx/main/models/inventory.py | 6 ++ awx/main/models/jobs.py | 51 ++++++++++++++++ awx/main/models/projects.py | 6 ++ awx/main/tasks.py | 111 +++++------------------------------ awx/settings/defaults.py | 1 + 6 files changed, 87 insertions(+), 98 deletions(-) diff --git a/awx/main/models/base.py b/awx/main/models/base.py index e6653881cf..0182cee98d 100644 --- a/awx/main/models/base.py +++ b/awx/main/models/base.py @@ -366,7 +366,7 @@ class CommonTask(PrimordialModel): def _get_passwords_needed_to_start(self): return [] - def start(self, **kwargs): + def start_signature(self, **kwargs): task_class = self._get_task_class() if not self.can_start: return False @@ -377,7 +377,13 @@ class CommonTask(PrimordialModel): self.status = 'pending' self.save(update_fields=['status']) transaction.commit() - task_result = task_class().delay(self.pk, **opts) + task_actual = task_class().si(self.pk, **opts) + return task_actual + + def start(self, **kwargs): + task_actual = self.start_signature(**kwargs) + # TODO: Callback for status + task_result = task_actual.delay() # Reload instance from database so we don't clobber results from task # (mainly from tests when using Django 1.4.x). instance = self.__class__.objects.get(pk=self.pk) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index dfadfd8bd4..6867fe251d 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -685,6 +685,12 @@ class InventorySource(PrimordialModel): # FIXME: Prevent update when another one is active! return bool(self.source) + def update_signature(self, **kwargs): + if self.can_update: + inventory_update = self.inventory_updates.create() + inventory_update_sig = inventory_update.start_signature() + return (inventory_update, inventory_update_sig) + def update(self, **kwargs): if self.can_update: inventory_update = self.inventory_updates.create() diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index d22099c676..02a5c77cb5 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -18,6 +18,7 @@ import yaml # Django from django.conf import settings from django.db import models +from django.db import transaction from django.utils.translation import ugettext_lazy as _ from django.core.exceptions import ValidationError, NON_FIELD_ERRORS from django.core.urlresolvers import reverse @@ -30,6 +31,11 @@ from jsonfield import JSONField # AWX from awx.main.models.base import * +# Celery +from celery import chain + +logger = logging.getLogger('awx.main.models.jobs') + __all__ = ['JobTemplate', 'Job', 'JobHostSummary', 'JobEvent'] @@ -328,6 +334,51 @@ class Job(CommonTask): def processed_hosts(self): return self._get_hosts(job_host_summaries__processed__gt=0) + def start(self, **kwargs): + task_class = self._get_task_class() + if not self.can_start: + return False + needed = self._get_passwords_needed_to_start() + opts = dict([(field, kwargs.get(field, '')) for field in needed]) + if not all(opts.values()): + return False + self.status = 'waiting' + self.save(update_fields=['status']) + transaction.commit() + + runnable_tasks = [] + inventory_updates_actual = [] + project_update_actual = None + + project = self.project + inventory = self.inventory + is_qs = inventory.inventory_sources.filter(active=True, update_on_launch=True) + if project.scm_update_on_launch: + # TODO: We assume these return a tuple but not on error + project_update, project_update_sig = project.update_signature() + if not project_update: + # TODO: Set error here + pass + else: + project_update_actual = project_update + # TODO: append a callback to gather the status? + runnable_tasks.append(project_update_sig) + # TODO: need to add celery task id to proj update instance + if is_qs.count(): + for inventory_source in is_qs: + # TODO: We assume these return a tuple but not on error + inventory_update, inventory_update_sig = inventory_source.update_signature() + if not inventory_update: + # TODO: Set error here + pass + else: + inventory_updates_actual.append(inventory_update) + runnable_tasks.append(inventory_update_sig) + job_actual = task_class().si(self.pk, **opts) + runnable_tasks.append(job_actual) + print runnable_tasks + res = chain(runnable_tasks)() + return True class JobHostSummary(BaseModel): ''' diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 7a531a9d10..9738d2627f 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -282,6 +282,12 @@ class Project(CommonModel): # FIXME: Prevent update when another one is active! return bool(self.scm_type)# and not self.current_update) + def update_signature(self, **kwargs): + if self.can_update: + project_update = self.project_updates.create() + project_update_sig = project_update.start_signature() + return (project_update, project_updaate_sig) + def update(self, **kwargs): if self.can_update: project_update = self.project_updates.create() diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 7b2702cea3..2b31fe8151 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -108,6 +108,7 @@ class BaseTask(Task): 'yes': 'yes', 'no': 'no', '': '', + } def build_env(self, instance, **kwargs): @@ -208,10 +209,12 @@ class BaseTask(Task): # we have a way to know the task is still running, otherwise the # post_run_hook below would cancel long-running tasks that are # really still active). - instance = self.update_model(instance.pk, status='running') - if instance.cancel_flag: - child.close(True) - canceled = True + #TODO: Find replacement for cancel flag + #TODO: Something about checking celery status + # instance = self.update_model(instance.pk, status='running') + # if instance.cancel_flag: + # child.close(True) + # canceled = True # FIXME: Find a way to determine if task is hung waiting at a prompt. if idle_timeout and (time.time() - last_stdout_update) > idle_timeout: child.close(True) @@ -444,98 +447,14 @@ class RunJob(BaseTask): ''' Hook for checking job before running. ''' - project_update = None - inventory_updates = None - while True: - pk = job.pk - if job.status in ('pending', 'waiting'): - project = job.project - pu_qs = project.project_updates.filter(status__in=('pending', 'running')) - inventory = job.inventory - base_iu_qs = InventoryUpdate.objects.filter(inventory_source__inventory=inventory) - iu_qs = base_iu_qs.filter(status__in=('pending', 'running')) - is_qs = inventory.inventory_sources.filter(active=True, update_on_launch=True) - # Refresh the current project_update instance (if set). - if project_update: - try: - project_update = project.project_updates.filter(pk=project_update.pk)[0] - except IndexError: - msg = 'Unable to check project update.' - job = self.update_model(pk, status='error', - result_traceback=msg) - return False - # Refresh the current inventory_update instance(s) (if set). - if inventory_updates: - inventory_update_pks = [x.pk for x in inventory_updates] - inventory_updates = list(base_iu_qs.filter(pk__in=inventory_update_pks)) - - # If the job needs to update the project first (and there is no - # specific project update defined). - if not project_update and project.scm_update_on_launch: - job = self.update_model(pk, status='waiting') - try: - project_update = pu_qs[0] - except IndexError: - project_update = project.update() - if not project_update: - msg = 'Unable to update project before launch.' - job = self.update_model(pk, status='error', - result_traceback=msg) - return False - #print 'job %d waiting on project update %d' % (pk, project_update.pk) - time.sleep(2.0) - # If the job needs to update any inventory first (and there are - # no current inventory updates pending). - elif inventory_updates is None and is_qs.count(): - job = self.update_model(pk, status='waiting') - inventory_updates = [] - msgs = [] - for inventory_source in is_qs: - try: - inventory_update = iu_qs.filter(inventory_source=inventory_source)[0] - except IndexError: - inventory_update = inventory_source.update() - if not inventory_update: - msgs.append('Unable to update inventory source %d before launch' % inventory_source.pk) - continue - inventory_updates.append(inventory_update) - if msgs: - msg = '\n'.join(msgs) - job = self.update_model(pk, status='error', - result_traceback=msg) - return False - time.sleep(2.0) - # If project update has failed, abort the job. - elif project_update and project_update.failed: - msg = 'Project update %d failed with status = %s.' % (project_update.pk, project_update.status) - job = self.update_model(pk, status='error', - result_traceback=msg) - return False - # If any inventory update has failed, abort the job. - elif inventory_updates and any([x.failed for x in inventory_updates]): - msgs = [] - for inventory_update in inventory_updates: - if inventory_update.failed: - msgs.append('Inventory update %d failed with status = %s.' % (inventory_update.pk, inventory_update.status)) - if msgs: - msg = '\n'.join(msgs) - job = self.update_model(pk, status='error', - result_traceback=msg) - return False - # Check if blocked by any other active project or inventory updates. - elif pu_qs.count() or iu_qs.count(): - #print 'job %d waiting on' % pk, pu_qs - job = self.update_model(pk, status='waiting') - time.sleep(4.0) - # Otherwise continue running the job. - else: - job = self.update_model(pk, status='pending') - return True - elif job.cancel_flag: - job = self.update_model(pk, status='canceled') - return False - else: - return False + if job.status in ('pending', 'waiting'): + job = self.update_model(job.pk, status='pending') + return True + elif job.cancel_flag: + job = self.update_model(job.pk, status='canceled') + return False + else: + return False def post_run_hook(self, job, **kwargs): ''' diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 1498d9ef27..fca79f1480 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -282,6 +282,7 @@ CELERYD_TASK_TIME_LIMIT = None CELERYD_TASK_SOFT_TIME_LIMIT = None CELERYBEAT_SCHEDULER = 'djcelery.schedulers.DatabaseScheduler' CELERYBEAT_MAX_LOOP_INTERVAL = 60 +CELERY_RESULT_BACKEND = 'djcelery.backends.database:DatabaseBackend' # Any ANSIBLE_* settings will be passed to the subprocess environment by the # celery task. From 654a7e6d07d9289386a78b2e840d5a5f4c2c8768 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 27 Jan 2014 12:20:21 -0500 Subject: [PATCH 04/32] Leave another TODO on the job runner, fix a misspelling on the project update hook --- awx/main/models/jobs.py | 1 + awx/main/models/projects.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 02a5c77cb5..d0ac720b43 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -342,6 +342,7 @@ class Job(CommonTask): opts = dict([(field, kwargs.get(field, '')) for field in needed]) if not all(opts.values()): return False + # TODO: This is temporary to allow a dependent task to continue self.status = 'waiting' self.save(update_fields=['status']) transaction.commit() diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 9738d2627f..4f36f00405 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -286,7 +286,7 @@ class Project(CommonModel): if self.can_update: project_update = self.project_updates.create() project_update_sig = project_update.start_signature() - return (project_update, project_updaate_sig) + return (project_update, project_update_sig) def update(self, **kwargs): if self.can_update: From 47950f28a722233f6644dbf1b2ed58ba99684f06 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 29 Jan 2014 14:04:07 -0500 Subject: [PATCH 05/32] Complete the new task job dependency system and add post-run error handler --- awx/main/models/base.py | 2 ++ awx/main/models/jobs.py | 29 +++++++++-------- awx/main/tasks.py | 69 ++++++++++++++++++++--------------------- 3 files changed, 52 insertions(+), 48 deletions(-) diff --git a/awx/main/models/base.py b/awx/main/models/base.py index 0182cee98d..02ae73519d 100644 --- a/awx/main/models/base.py +++ b/awx/main/models/base.py @@ -367,6 +367,8 @@ class CommonTask(PrimordialModel): return [] def start_signature(self, **kwargs): + from awx.main.tasks import handle_work_error + task_class = self._get_task_class() if not self.can_start: return False diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index d0ac720b43..5deabe4f4d 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -335,6 +335,7 @@ class Job(CommonTask): return self._get_hosts(job_host_summaries__processed__gt=0) def start(self, **kwargs): + from awx.main.tasks import handle_work_error task_class = self._get_task_class() if not self.can_start: return False @@ -348,6 +349,7 @@ class Job(CommonTask): transaction.commit() runnable_tasks = [] + run_tasks = [] inventory_updates_actual = [] project_update_actual = None @@ -355,28 +357,29 @@ class Job(CommonTask): inventory = self.inventory is_qs = inventory.inventory_sources.filter(active=True, update_on_launch=True) if project.scm_update_on_launch: - # TODO: We assume these return a tuple but not on error - project_update, project_update_sig = project.update_signature() - if not project_update: + project_update_details = project.update_signature() + if not project_update_details: # TODO: Set error here pass else: - project_update_actual = project_update - # TODO: append a callback to gather the status? - runnable_tasks.append(project_update_sig) - # TODO: need to add celery task id to proj update instance + runnable_tasks.append({'obj': project_update_details[0], + 'sig': project_update_details[1], + 'type': 'project_update'}) if is_qs.count(): for inventory_source in is_qs: - # TODO: We assume these return a tuple but not on error - inventory_update, inventory_update_sig = inventory_source.update_signature() + inventory_update_details = inventory_source.update_signature() if not inventory_update: # TODO: Set error here pass else: - inventory_updates_actual.append(inventory_update) - runnable_tasks.append(inventory_update_sig) - job_actual = task_class().si(self.pk, **opts) - runnable_tasks.append(job_actual) + runnable_tasks.append({'obj': inventory_update_details[0], + 'sig': inventory_update_details[1], + 'type': 'inventory_update'}) + thisjob = {'type': 'job', 'id': self.id} + for idx in xrange(len(runnable_tasks)): + dependent_tasks = [{'type': r['type'], 'id': r['obj'].id} for r in runnable_tasks[idx:]] + [thisjob] + run_tasks.append(runnable_tasks[idx]['sig'].set(link_error=handle_work_error.s(subtasks=dependent_tasks))) + run_tasks.append(task_class().si(self.pk, **opts).set(link_error=handle_work_error.s(subtasks=[thisjob]))) print runnable_tasks res = chain(runnable_tasks)() return True diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 2b31fe8151..04227572f9 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -23,7 +23,7 @@ import uuid import pexpect # Celery -from celery import Task +from celery import Task, task # Django from django.conf import settings @@ -35,12 +35,32 @@ from django.utils.timezone import now from awx.main.models import Job, JobEvent, ProjectUpdate, InventoryUpdate from awx.main.utils import get_ansible_version, decrypt_field, update_scm_url -__all__ = ['RunJob', 'RunProjectUpdate', 'RunInventoryImport'] +__all__ = ['RunJob', 'RunProjectUpdate', 'RunInventoryUpdate', 'handle_work_error'] logger = logging.getLogger('awx.main.tasks') # FIXME: Cleanly cancel task when celery worker is stopped. +@task(bind=True) +def handle_work_error(self, task_id, subtasks=None): + print('Executing error task id %s, subtasks: %s' % (str(self.request.id), str(subtasks))) + if subtasks is not None: + for each_task in subtasks: + if each_task['type'] == 'project_update': + instance = ProjectUpdate.objects.get(id=each_task['id']) + elif each_task['type'] == 'inventory_update': + instance = InventoryUpdate.objects.get(id=each_task['id']) + elif each_task['type': 'job']: + instance = Job.objects.get(id=each_task['id']) + else: + # Unknown task type + break + if instance.celery_task_id != instance.celery_task_id: + instance.status = 'failed' + instance.failed = True + instance.result_traceback = "Previous Task Failed: %s" % str(subtasks) + instance.save() + class BaseTask(Task): name = None @@ -205,13 +225,8 @@ class BaseTask(Task): if logfile_pos != logfile.tell(): logfile_pos = logfile.tell() last_stdout_update = time.time() - # Update instance status here (also updates modified timestamp, so - # we have a way to know the task is still running, otherwise the - # post_run_hook below would cancel long-running tasks that are - # really still active). #TODO: Find replacement for cancel flag #TODO: Something about checking celery status - # instance = self.update_model(instance.pk, status='running') # if instance.cancel_flag: # child.close(True) # canceled = True @@ -237,40 +252,14 @@ class BaseTask(Task): return True def post_run_hook(self, instance, **kwargs): - ''' - Hook for actions to run after job/task has completed. - ''' - # Cleanup instances that appear to be stuck. - try: - stuck_task_timeout = int(getattr(settings, 'STUCK_TASK_TIMEOUT', 300)) - except (TypeError, ValueError): - stuck_task_timeout = 0 - if stuck_task_timeout <= 0: - return - # Never less than 30 seconds so we're not messing with active tasks. - stuck_task_timeout = max(stuck_task_timeout, 30) - cutoff = now() - datetime.timedelta(seconds=stuck_task_timeout) - qs = self.model.objects.filter(status__in=('new', 'waiting', 'running')) - qs = qs.filter(modified__lt=cutoff) - for obj in qs: - # If new, created but never started. If waiting or running, the - # modified timestamp should updated regularly, else the task is - # probably stuck. - # If pending, we could be started but celeryd is not running, or - # we're waiting for an open slot in celeryd -- in either case we - # shouldn't necessarily cancel the task. Slim chance that somehow - # the task was started, picked up by celery, but hit an error - # before we could update the status. - obj.status = 'canceled' - obj.result_traceback += '\nCanceled stuck %s.' % unicode(self.model._meta.verbose_name) - obj.save(update_fields=['status', 'result_traceback']) + pass @transaction.commit_on_success def run(self, pk, **kwargs): ''' Run the job/task and capture its output. ''' - instance = self.update_model(pk) + instance = self.update_model(pk, status='pending', celery_task_id=self.request.id) status, stdout, tb = 'error', '', '' output_replacements = [] try: @@ -308,6 +297,10 @@ class BaseTask(Task): result_traceback=tb, output_replacements=output_replacements) self.post_run_hook(instance, **kwargs) + if status != 'successful': + # Raising an exception will mark the job as 'failed' in celery + # and will stop a task chain from continuing to execute + raise Exception("Task %s(pk:%s) encountered an error" % (str(self.model.__class__), str(pk))) class RunJob(BaseTask): ''' @@ -434,6 +427,9 @@ class RunJob(BaseTask): (job.project.local_path, root)) return cwd + def get_idle_timeout(self): + return getattr(settings, 'JOB_RUN_IDLE_TIMEOUT', 300) + def get_password_prompts(self): d = super(RunJob, self).get_password_prompts() d[re.compile(r'^Enter passphrase for .*:\s*?$', re.M)] = 'ssh_key_unlock' @@ -801,6 +797,9 @@ class RunInventoryUpdate(BaseTask): def build_cwd(self, inventory_update, **kwargs): return self.get_path_to('..', 'plugins', 'inventory') + def get_idle_timeout(self): + return getattr(settings, 'INVENTORY_UPDATE_IDLE_TIMEOUT', 300) + def pre_run_check(self, inventory_update, **kwargs): ''' Hook for checking inventory update before running. From ee246aa8c441df185380979189d2c1194c3d0b1d Mon Sep 17 00:00:00 2001 From: Chris Church Date: Mon, 27 Jan 2014 18:37:51 -0500 Subject: [PATCH 06/32] AC-984 Prevent signal handlers from being run unnecessarily when deleting inventory, remove unnecessary extra queries, use update_fields when possible. --- awx/api/generics.py | 4 +- awx/api/views.py | 6 ++ awx/main/middleware.py | 30 +++--- awx/main/models/activity_stream.py | 11 ++ awx/main/models/base.py | 10 +- awx/main/models/inventory.py | 15 +-- awx/main/signals.py | 162 ++++++++++++++++++----------- 7 files changed, 153 insertions(+), 85 deletions(-) diff --git a/awx/api/generics.py b/awx/api/generics.py index 37618c5b27..3c098820ff 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -6,7 +6,7 @@ import inspect import json # Django -from django.http import HttpResponse, Http404 +from django.http import Http404 from django.contrib.auth.models import User from django.shortcuts import get_object_or_404 from django.template.loader import render_to_string @@ -428,4 +428,4 @@ class RetrieveUpdateDestroyAPIView(RetrieveUpdateAPIView, generics.RetrieveUpdat obj.mark_inactive() else: raise NotImplementedError('destroy() not implemented yet for %s' % obj) - return HttpResponse(status=204) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/awx/api/views.py b/awx/api/views.py index ac7305f5be..f2aa3530b7 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -34,6 +34,7 @@ from awx.main.licenses import LicenseReader from awx.main.models import * from awx.main.utils import * from awx.main.access import get_user_queryset +from awx.main.signals import ignore_inventory_computed_fields, ignore_inventory_group_removal from awx.api.authentication import JobTaskAuthentication from awx.api.permissions import * from awx.api.serializers import * @@ -619,6 +620,11 @@ class InventoryDetail(RetrieveUpdateDestroyAPIView): model = Inventory serializer_class = InventorySerializer + def destroy(self, request, *args, **kwargs): + with ignore_inventory_computed_fields(): + with ignore_inventory_group_removal(): + return super(InventoryDetail, self).destroy(request, *args, **kwargs) + class InventoryActivityStreamList(SubListAPIView): model = ActivityStream diff --git a/awx/main/middleware.py b/awx/main/middleware.py index ce6a88b13f..c8f91f2e75 100644 --- a/awx/main/middleware.py +++ b/awx/main/middleware.py @@ -8,17 +8,18 @@ from django.db import IntegrityError from django.utils.functional import curry from awx.main.models import ActivityStream, AuthToken import json +import threading import uuid import urllib2 import logging logger = logging.getLogger('awx.main.middleware') -class ActivityStreamMiddleware(object): +class ActivityStreamMiddleware(threading.local): def __init__(self): self.disp_uid = None - self.instances = [] + self.instance_ids = [] def process_request(self, request): if hasattr(request, 'user') and hasattr(request.user, 'is_authenticated') and request.user.is_authenticated(): @@ -28,6 +29,7 @@ class ActivityStreamMiddleware(object): set_actor = curry(self.set_actor, user) self.disp_uid = str(uuid.uuid1()) + self.instance_ids = [] post_save.connect(set_actor, sender=ActivityStream, dispatch_uid=self.disp_uid, weak=False) def process_response(self, request, response): @@ -35,31 +37,27 @@ class ActivityStreamMiddleware(object): drf_user = getattr(drf_request, 'user', None) if self.disp_uid is not None: post_save.disconnect(dispatch_uid=self.disp_uid) - for instance_id in self.instances: - instance = ActivityStream.objects.filter(id=instance_id) - if instance.exists(): - instance = instance[0] - else: - logger.debug("Failed to look up Activity Stream instance for id : " + str(instance_id)) - continue - if drf_user is not None and drf_user.__class__ != AnonymousUser: + for instance in ActivityStream.objects.filter(id__in=self.instance_ids): + if drf_user and drf_user.pk: instance.actor = drf_user try: - instance.save() + instance.save(update_fields=['actor']) except IntegrityError, e: - logger.debug("Integrity Error saving Activity Stream instance for id : " + str(instance_id)) + logger.debug("Integrity Error saving Activity Stream instance for id : " + str(instance.id)) # else: # obj1_type_actual = instance.object1_type.split(".")[-1] # if obj1_type_actual in ("InventoryUpdate", "ProjectUpdate", "Job") and instance.id is not None: # instance.delete() + + self.instance_ids = [] return response def set_actor(self, user, sender, instance, **kwargs): if sender == ActivityStream: - if isinstance(user, User) and instance.user is None: + if isinstance(user, User) and instance.actor is None: instance.actor = user - instance.save() + instance.save(update_fields=['actor']) else: - if instance.id not in self.instances: - self.instances.append(instance.id) + if instance.id not in self.instance_ids: + self.instance_ids.append(instance.id) diff --git a/awx/main/models/activity_stream.py b/awx/main/models/activity_stream.py index ed7a1c5fba..a41f14f0d1 100644 --- a/awx/main/models/activity_stream.py +++ b/awx/main/models/activity_stream.py @@ -51,3 +51,14 @@ class ActivityStream(models.Model): def get_absolute_url(self): return reverse('api:activity_stream_detail', args=(self.pk,)) + + def save(self, *args, **kwargs): + # For compatibility with Django 1.4.x, attempt to handle any calls to + # save that pass update_fields. + try: + super(ActivityStream, self).save(*args, **kwargs) + except TypeError: + if 'update_fields' not in kwargs: + raise + kwargs.pop('update_fields') + super(ActivityStream, self).save(*args, **kwargs) diff --git a/awx/main/models/base.py b/awx/main/models/base.py index 02ae73519d..7baf81e93b 100644 --- a/awx/main/models/base.py +++ b/awx/main/models/base.py @@ -193,14 +193,20 @@ class PrimordialModel(BaseModel): tags = TaggableManager(blank=True) - def mark_inactive(self, save=True): + def mark_inactive(self, save=True, update_fields=None): '''Use instead of delete to rename and mark inactive.''' + update_fields = update_fields or [] if self.active: if 'name' in self._meta.get_all_field_names(): self.name = "_deleted_%s_%s" % (now().isoformat(), self.name) + if 'name' not in update_fields: + update_fields.append('name') self.active = False + if 'active' not in update_fields: + update_fields.append('active') if save: - self.save() + self.save(update_fields=update_fields) + return update_fields def clean_description(self): # Description should always be empty string, never null. diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 6867fe251d..acd969b2e9 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -100,14 +100,15 @@ class Inventory(CommonModel): ''' When marking inventory inactive, also mark hosts and groups inactive. ''' + from awx.main.signals import ignore_inventory_computed_fields + with ignore_inventory_computed_fields(): + for host in self.hosts.filter(active=True): + host.mark_inactive() + for group in self.groups.filter(active=True): + group.mark_inactive() + for inventory_source in self.inventory_sources.filter(active=True): + inventory_source.mark_inactive() super(Inventory, self).mark_inactive(save=save) - for host in self.hosts.filter(active=True): - host.mark_inactive() - for group in self.groups.filter(active=True): - group.mark_inactive() - group.inventory_source.mark_inactive() - for inventory_source in self.inventory_sources.filter(active=True): - inventory_source.mark_inactive() variables_dict = VarsDictProperty('variables') diff --git a/awx/main/signals.py b/awx/main/signals.py index efb00f69a2..37b1923f3b 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -24,7 +24,7 @@ logger = logging.getLogger('awx.main.signals') # or marked inactive, when a Host-Group or Group-Group relationship is updated, # or when a Job is deleted or marked inactive. -_inventory_updating = threading.local() +_inventory_updates = threading.local() @contextlib.contextmanager def ignore_inventory_computed_fields(): @@ -32,49 +32,62 @@ def ignore_inventory_computed_fields(): Context manager to ignore updating inventory computed fields. ''' try: - previous_value = getattr(_inventory_updating, 'is_updating', False) - _inventory_updating.is_updating = True + previous_value = getattr(_inventory_updates, 'is_updating', False) + _inventory_updates.is_updating = True yield finally: - _inventory_updating.is_updating = previous_value + _inventory_updates.is_updating = previous_value + +@contextlib.contextmanager +def ignore_inventory_group_removal(): + ''' + Context manager to ignore moving groups/hosts when group is deleted. + ''' + try: + previous_value = getattr(_inventory_updates, 'is_removing', False) + _inventory_updates.is_removing = True + yield + finally: + _inventory_updates.is_removing = previous_value def update_inventory_computed_fields(sender, **kwargs): ''' Signal handler and wrapper around inventory.update_computed_fields to prevent unnecessary recursive calls. ''' - if not getattr(_inventory_updating, 'is_updating', False): - instance = kwargs['instance'] - if sender == Group.hosts.through: - sender_name = 'group.hosts' - elif sender == Group.parents.through: - sender_name = 'group.parents' - elif sender == Host.inventory_sources.through: - sender_name = 'host.inventory_sources' - elif sender == Group.inventory_sources.through: - sender_name = 'group.inventory_sources' - else: - sender_name = unicode(sender._meta.verbose_name) - if kwargs['signal'] == post_save: - if sender == Job and instance.active: - return - sender_action = 'saved' - elif kwargs['signal'] == post_delete: - sender_action = 'deleted' - elif kwargs['signal'] == m2m_changed and kwargs['action'] in ('post_add', 'post_remove', 'post_clear'): - sender_action = 'changed' - else: + if getattr(_inventory_updates, 'is_updating', False): + return + instance = kwargs['instance'] + if sender == Group.hosts.through: + sender_name = 'group.hosts' + elif sender == Group.parents.through: + sender_name = 'group.parents' + elif sender == Host.inventory_sources.through: + sender_name = 'host.inventory_sources' + elif sender == Group.inventory_sources.through: + sender_name = 'group.inventory_sources' + else: + sender_name = unicode(sender._meta.verbose_name) + if kwargs['signal'] == post_save: + if sender == Job and instance.active: return - logger.debug('%s %s, updating inventory computed fields: %r %r', - sender_name, sender_action, sender, kwargs) - with ignore_inventory_computed_fields(): - try: - inventory = instance.inventory - except Inventory.DoesNotExist: - pass - else: - update_hosts = issubclass(sender, Job) - inventory.update_computed_fields(update_hosts=update_hosts) + sender_action = 'saved' + elif kwargs['signal'] == post_delete: + sender_action = 'deleted' + elif kwargs['signal'] == m2m_changed and kwargs['action'] in ('post_add', 'post_remove', 'post_clear'): + sender_action = 'changed' + else: + return + logger.debug('%s %s, updating inventory computed fields: %r %r', + sender_name, sender_action, sender, kwargs) + with ignore_inventory_computed_fields(): + try: + inventory = instance.inventory + except Inventory.DoesNotExist: + pass + else: + update_hosts = issubclass(sender, Job) + inventory.update_computed_fields(update_hosts=update_hosts) post_save.connect(update_inventory_computed_fields, sender=Host) post_delete.connect(update_inventory_computed_fields, sender=Host) @@ -94,32 +107,50 @@ post_delete.connect(update_inventory_computed_fields, sender=InventorySource) @receiver(pre_delete, sender=Group) def save_related_pks_before_group_delete(sender, **kwargs): + if getattr(_inventory_updates, 'is_removing', False): + return instance = kwargs['instance'] + instance._saved_inventory_pk = instance.inventory.pk instance._saved_parents_pks = set(instance.parents.values_list('pk', flat=True)) instance._saved_hosts_pks = set(instance.hosts.values_list('pk', flat=True)) instance._saved_children_pks = set(instance.children.values_list('pk', flat=True)) @receiver(post_delete, sender=Group) def migrate_children_from_deleted_group_to_parent_groups(sender, **kwargs): + if getattr(_inventory_updates, 'is_removing', False): + return instance = kwargs['instance'] parents_pks = getattr(instance, '_saved_parents_pks', []) hosts_pks = getattr(instance, '_saved_hosts_pks', []) children_pks = getattr(instance, '_saved_children_pks', []) - for parent_group in Group.objects.filter(pk__in=parents_pks): - for child_host in Host.objects.filter(pk__in=hosts_pks): - logger.debug('adding host %s to parent %s after group deletion', - child_host, parent_group) - parent_group.hosts.add(child_host) - for child_group in Group.objects.filter(pk__in=children_pks): - logger.debug('adding group %s to parent %s after group deletion', - child_group, parent_group) - parent_group.children.add(child_group) + with ignore_inventory_group_removal(): + with ignore_inventory_computed_fields(): + if parents_pks: + for parent_group in Group.objects.filter(pk__in=parents_pks, active=True): + for child_host in Host.objects.filter(pk__in=hosts_pks, active=True): + logger.debug('adding host %s to parent %s after group deletion', + child_host, parent_group) + parent_group.hosts.add(child_host) + for child_group in Group.objects.filter(pk__in=children_pks, active=True): + logger.debug('adding group %s to parent %s after group deletion', + child_group, parent_group) + parent_group.children.add(child_group) + inventory_pk = getattr(instance, '_saved_inventory_pk', None) + if inventory_pk: + try: + inventory = Inventory.objects.get(pk=inventory_pk, active=True) + inventory.update_computed_fields() + except Inventory.DoesNotExist: + pass @receiver(pre_save, sender=Group) def save_related_pks_before_group_marked_inactive(sender, **kwargs): + if getattr(_inventory_updates, 'is_removing', False): + return instance = kwargs['instance'] if not instance.pk or instance.active: return + instance._saved_inventory_pk = instance.inventory.pk instance._saved_parents_pks = set(instance.parents.values_list('pk', flat=True)) instance._saved_hosts_pks = set(instance.hosts.values_list('pk', flat=True)) instance._saved_children_pks = set(instance.children.values_list('pk', flat=True)) @@ -127,26 +158,41 @@ def save_related_pks_before_group_marked_inactive(sender, **kwargs): @receiver(post_save, sender=Group) def migrate_children_from_inactive_group_to_parent_groups(sender, **kwargs): + if getattr(_inventory_updates, 'is_removing', False): + return instance = kwargs['instance'] if instance.active: return parents_pks = getattr(instance, '_saved_parents_pks', []) hosts_pks = getattr(instance, '_saved_hosts_pks', []) children_pks = getattr(instance, '_saved_children_pks', []) - for parent_group in Group.objects.filter(pk__in=parents_pks): - for child_host in Host.objects.filter(pk__in=hosts_pks): - logger.debug('moving host %s to parent %s after marking group %s inactive', - child_host, parent_group, instance) - parent_group.hosts.add(child_host) - for child_group in Group.objects.filter(pk__in=children_pks): - logger.debug('moving group %s to parent %s after marking group %s inactive', - child_group, parent_group, instance) - parent_group.children.add(child_group) - parent_group.children.remove(instance) - inventory_source_pk = getattr(instance, '_saved_inventory_source_pk', None) - if inventory_source_pk: - inventory_source = InventorySource.objects.get(pk=inventory_source_pk) - inventory_source.mark_inactive() + with ignore_inventory_group_removal(): + with ignore_inventory_computed_fields(): + if parents_pks: + for parent_group in Group.objects.filter(pk__in=parents_pks, active=True): + for child_host in Host.objects.filter(pk__in=hosts_pks, active=True): + logger.debug('moving host %s to parent %s after marking group %s inactive', + child_host, parent_group, instance) + parent_group.hosts.add(child_host) + for child_group in Group.objects.filter(pk__in=children_pks, active=True): + logger.debug('moving group %s to parent %s after marking group %s inactive', + child_group, parent_group, instance) + parent_group.children.add(child_group) + parent_group.children.remove(instance) + inventory_source_pk = getattr(instance, '_saved_inventory_source_pk', None) + if inventory_source_pk: + try: + inventory_source = InventorySource.objects.get(pk=inventory_source_pk, active=True) + inventory_source.mark_inactive() + except InventorySource.DoesNotExist: + pass + inventory_pk = getattr(instance, '_saved_inventory_pk', None) + if inventory_pk: + try: + inventory = Inventory.objects.get(pk=inventory_pk, active=True) + inventory.update_computed_fields() + except Inventory.DoesNotExist: + pass # Update host pointers to last_job and last_job_host_summary when a job is # marked inactive or deleted. From e27ed61fc84c7cc4a7e512c621f375d0a1cf9a4f Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Tue, 28 Jan 2014 01:02:34 -0500 Subject: [PATCH 07/32] AC-983 long group names, AC-970 don't hightlight required fields in red, AC-966 fixed ansibleworks links to point to ansible, AC-733 new pagination widget, page and total rows. --- awx/ui/static/js/app.js | 3 +- awx/ui/static/js/controllers/Credentials.js | 4 +- awx/ui/static/js/controllers/Home.js | 6 +- awx/ui/static/js/controllers/Inventories.js | 9 +- awx/ui/static/js/controllers/JobTemplates.js | 4 +- awx/ui/static/js/controllers/Organizations.js | 4 +- awx/ui/static/js/controllers/Projects.js | 4 +- awx/ui/static/js/controllers/Teams.js | 4 +- awx/ui/static/js/controllers/Users.js | 4 +- awx/ui/static/js/helpers/Groups.js | 2 +- awx/ui/static/js/helpers/Hosts.js | 6 +- awx/ui/static/js/helpers/Lookup.js | 2 +- awx/ui/static/js/helpers/PaginationHelpers.js | 169 ++++++++++++++++++ awx/ui/static/js/helpers/inventory.js | 6 +- awx/ui/static/js/helpers/paginate.js | 82 --------- awx/ui/static/js/helpers/refresh-related.js | 18 +- awx/ui/static/js/helpers/refresh.js | 16 +- awx/ui/static/js/helpers/related-paginate.js | 67 ------- awx/ui/static/js/helpers/search.js | 147 ++++----------- awx/ui/static/js/helpers/teams.js | 2 +- awx/ui/static/js/lists/InventoryGroups.js | 5 +- awx/ui/static/js/lists/InventoryHosts.js | 6 - awx/ui/static/js/widgets/Stream.js | 41 +++-- awx/ui/static/less/ansible-ui.less | 41 ++++- awx/ui/static/lib/ansible/InventoryTree.js | 1 - awx/ui/static/lib/ansible/Utilities.js | 46 ++++- awx/ui/static/lib/ansible/form-generator.js | 16 +- .../static/lib/ansible/generator-helpers.js | 99 ++++------ awx/ui/static/lib/ansible/list-generator.js | 6 +- awx/ui/templates/ui/index.html | 3 +- 30 files changed, 397 insertions(+), 426 deletions(-) create mode 100644 awx/ui/static/js/helpers/PaginationHelpers.js delete mode 100644 awx/ui/static/js/helpers/paginate.js delete mode 100644 awx/ui/static/js/helpers/related-paginate.js diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index c636181275..51937f7bca 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -21,9 +21,8 @@ angular.module('ansible', [ 'PromptDialog', 'ApiLoader', 'RelatedSearchHelper', - 'RelatedPaginateHelper', 'SearchHelper', - 'PaginateHelper', + 'PaginationHelpers', 'RefreshHelper', 'AdminListDefinition', 'AWDirectives', diff --git a/awx/ui/static/js/controllers/Credentials.js b/awx/ui/static/js/controllers/Credentials.js index 21404ec788..a8bbedc751 100644 --- a/awx/ui/static/js/controllers/Credentials.js +++ b/awx/ui/static/js/controllers/Credentials.js @@ -82,7 +82,7 @@ function CredentialsList ($scope, $rootScope, $location, $log, $routeParams, Res LoadBreadCrumbs(); - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } scope.addCredential = function() { $location.path($location.path() + '/add'); @@ -406,7 +406,7 @@ function CredentialsEdit ($scope, $rootScope, $compile, $location, $log, $routeP callback: 'choicesReadyCredential' }); - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } // Save changes to the parent scope.formSave = function() { generator.clearApiErrors(); FormSave({ scope: scope, mode: 'edit' }) }; diff --git a/awx/ui/static/js/controllers/Home.js b/awx/ui/static/js/controllers/Home.js index dd2117585a..90f37d786a 100644 --- a/awx/ui/static/js/controllers/Home.js +++ b/awx/ui/static/js/controllers/Home.js @@ -69,7 +69,7 @@ function Home ($scope, $compile, $routeParams, $rootScope, $location, Wait, Obje ObjectCount({ scope: $scope, target: 'container3', dashboard: data}); }); - $scope.showActivity = function() { Stream(); } + $scope.showActivity = function() { Stream({ scope: $scope }); } $scope.refresh = function() { Wait('start'); @@ -218,7 +218,7 @@ function HomeGroups ($location, $routeParams, HomeGroupList, GenerateList, Proce LoadBreadCrumbs(); - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } scope.editGroup = function(group_id, inventory_id) { GroupsEdit({ scope: scope, group_id: group_id, inventory_id: inventory_id, groups_reload: false }); @@ -328,7 +328,7 @@ function HomeHosts ($location, $routeParams, HomeHostList, GenerateList, Process LoadBreadCrumbs(); - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } scope.toggle_host_enabled = function(id, sources) { ToggleHostEnabled({ host_id: id, external_source: sources, scope: scope }); } diff --git a/awx/ui/static/js/controllers/Inventories.js b/awx/ui/static/js/controllers/Inventories.js index 5f4c36bf3c..6547d9565c 100644 --- a/awx/ui/static/js/controllers/Inventories.js +++ b/awx/ui/static/js/controllers/Inventories.js @@ -129,7 +129,7 @@ function InventoriesList ($scope, $rootScope, $location, $log, $routeParams, Res } }); - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } scope.addInventory = function() { $location.path($location.path() + '/add'); @@ -500,15 +500,10 @@ function InventoriesEdit ($scope, $location, $routeParams, $compile, GenerateLis $scope.toggleHostEnabled = function(host_id, external_source) { ToggleHostEnabled({ scope: $scope, host_id: host_id, external_source: external_source }); } - - $scope.showHostActivity = function() { - var url = GetBasePath('activity_stream') + '?host__inventory__id=' + $scope.inventory_id; - Stream({ inventory_name: $scope.inventory_name, url: url }); - } $scope.showGroupActivity = function() { var url = GetBasePath('activity_stream') + '?group__inventory__id=' + $scope.inventory_id; - Stream({ inventory_name: $scope.inventory_name, url: url }); + Stream({ scope: $scope, inventory_name: $scope.inventory_name, url: url }); } $scope.showJobSummary = function(job_id) { diff --git a/awx/ui/static/js/controllers/JobTemplates.js b/awx/ui/static/js/controllers/JobTemplates.js index dd9ba5bd91..fe10b9cdf6 100644 --- a/awx/ui/static/js/controllers/JobTemplates.js +++ b/awx/ui/static/js/controllers/JobTemplates.js @@ -48,7 +48,7 @@ function JobTemplatesList ($scope, $rootScope, $location, $log, $routeParams, Re LoadBreadCrumbs(); - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } scope.addJobTemplate = function() { $location.path($location.path() + '/add'); @@ -646,7 +646,7 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route } }; - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } // Cancel scope.formReset = function() { diff --git a/awx/ui/static/js/controllers/Organizations.js b/awx/ui/static/js/controllers/Organizations.js index ae940fa77e..6e78ccd111 100644 --- a/awx/ui/static/js/controllers/Organizations.js +++ b/awx/ui/static/js/controllers/Organizations.js @@ -45,7 +45,7 @@ function OrganizationsList ($routeParams, $scope, $rootScope, $location, $log, R PaginateInit({ scope: scope, list: list, url: defaultUrl }); scope.search(list.iterator); - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } scope.addOrganization = function() { $location.path($location.path() + '/add'); @@ -220,7 +220,7 @@ function OrganizationsEdit ($scope, $rootScope, $compile, $location, $log, $rout }); }; - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } // Reset the form scope.formReset = function() { diff --git a/awx/ui/static/js/controllers/Projects.js b/awx/ui/static/js/controllers/Projects.js index e23623d3f7..b6f3a21f16 100644 --- a/awx/ui/static/js/controllers/Projects.js +++ b/awx/ui/static/js/controllers/Projects.js @@ -160,7 +160,7 @@ function ProjectsList ($scope, $rootScope, $location, $log, $routeParams, Rest, LoadBreadCrumbs(); - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } scope.addProject = function() { $location.path($location.path() + '/add'); @@ -642,7 +642,7 @@ function ProjectsEdit ($scope, $rootScope, $compile, $location, $log, $routePara }); }; - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } // Related set: Add button scope.add = function(set) { diff --git a/awx/ui/static/js/controllers/Teams.js b/awx/ui/static/js/controllers/Teams.js index c81d04121c..261a167f64 100644 --- a/awx/ui/static/js/controllers/Teams.js +++ b/awx/ui/static/js/controllers/Teams.js @@ -45,7 +45,7 @@ function TeamsList ($scope, $rootScope, $location, $log, $routeParams, Rest, Ale LoadBreadCrumbs(); - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } scope.addTeam = function() { $location.path($location.path() + '/add'); @@ -237,7 +237,7 @@ function TeamsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, { hdr: 'Error!', msg: 'Failed to retrieve team: ' + $routeParams.team_id + '. GET status: ' + status }); }); - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } // Save changes to the parent scope.formSave = function() { diff --git a/awx/ui/static/js/controllers/Users.js b/awx/ui/static/js/controllers/Users.js index 8f37b1c07f..50b157a2f9 100644 --- a/awx/ui/static/js/controllers/Users.js +++ b/awx/ui/static/js/controllers/Users.js @@ -45,7 +45,7 @@ function UsersList ($scope, $rootScope, $location, $log, $routeParams, Rest, Ale LoadBreadCrumbs(); - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } scope.addUser = function() { $location.path($location.path() + '/add'); @@ -300,7 +300,7 @@ function UsersEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, }); }; - scope.showActivity = function() { Stream(); } + scope.showActivity = function() { Stream({ scope: scope }); } // Cancel scope.formReset = function() { diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js index 2da171e82a..67322459ab 100644 --- a/awx/ui/static/js/helpers/Groups.js +++ b/awx/ui/static/js/helpers/Groups.js @@ -10,7 +10,7 @@ 'use strict'; angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'GroupListDefinition', - 'SearchHelper', 'PaginateHelper', 'ListGenerator', 'AuthService', 'GroupsHelper', + 'SearchHelper', 'PaginationHelpers', 'ListGenerator', 'AuthService', 'GroupsHelper', 'InventoryHelper', 'SelectionHelper', 'JobSubmissionHelper', 'RefreshHelper', 'PromptDialog', 'InventorySummaryHelpDefinition', 'CredentialsListDefinition', 'InventoryTree' diff --git a/awx/ui/static/js/helpers/Hosts.js b/awx/ui/static/js/helpers/Hosts.js index d99e9eba5c..74df35b788 100644 --- a/awx/ui/static/js/helpers/Hosts.js +++ b/awx/ui/static/js/helpers/Hosts.js @@ -8,9 +8,9 @@ */ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'HostListDefinition', - 'SearchHelper', 'PaginateHelper', 'ListGenerator', 'AuthService', 'HostsHelper', - 'InventoryHelper', 'RelatedSearchHelper','RelatedPaginateHelper', - 'InventoryFormDefinition', 'SelectionHelper', 'HostGroupsFormDefinition' + 'SearchHelper', 'PaginationHelpers', 'ListGenerator', 'AuthService', 'HostsHelper', + 'InventoryHelper', 'RelatedSearchHelper', 'InventoryFormDefinition', 'SelectionHelper', + 'HostGroupsFormDefinition' ]) diff --git a/awx/ui/static/js/helpers/Lookup.js b/awx/ui/static/js/helpers/Lookup.js index ac0dd7a203..5d3dfc4aab 100644 --- a/awx/ui/static/js/helpers/Lookup.js +++ b/awx/ui/static/js/helpers/Lookup.js @@ -14,7 +14,7 @@ * }) */ -angular.module('LookUpHelper', [ 'RestServices', 'Utilities', 'SearchHelper', 'PaginateHelper', 'ListGenerator', 'ApiLoader' ]) +angular.module('LookUpHelper', [ 'RestServices', 'Utilities', 'SearchHelper', 'PaginationHelpers', 'ListGenerator', 'ApiLoader' ]) .factory('LookUpInit', ['Alert', 'Rest', 'GenerateList', 'SearchInit', 'PaginateInit', 'GetBasePath', 'FormatDate', 'Empty', function(Alert, Rest, GenerateList, SearchInit, PaginateInit, GetBasePath, FormatDate, Empty) { return function(params) { diff --git a/awx/ui/static/js/helpers/PaginationHelpers.js b/awx/ui/static/js/helpers/PaginationHelpers.js new file mode 100644 index 0000000000..bcab66394c --- /dev/null +++ b/awx/ui/static/js/helpers/PaginationHelpers.js @@ -0,0 +1,169 @@ +/********************************************* + * Copyright (c) 2014 AnsibleWorks, Inc. + * + * PaginationHelpers.js + * + */ + +angular.module('PaginationHelpers', ['Utilities', 'RefreshHelper', 'RefreshRelatedHelper']) + + .factory('PageRangeSetup', ['Empty', function(Empty) { + return function(params) { + + var scope = params.scope; + var count = params.count; + var next = params.next; + var previous = params.previous; + var iterator = params.iterator; + + scope[iterator + '_page'] = 1; + scope[iterator + '_num_pages'] = Math.ceil((count / scope[iterator + '_page_size'])); + scope[iterator + '_num_pages'] = (scope[iterator + '_num_pages'] <= 0) ? 1 : scope[iterator + '_num_pages']; + scope[iterator + '_total_rows'] = count; + + // Which page are we on? + if ( Empty(next) && previous ) { + // no next page, but there is a previous page + scope[iterator + '_page'] = parseInt(previous.match(/page=\d+/)[0].replace(/page=/,'')) + 1; + } + else if ( next && Empty(previous) ) { + // next page available, but no previous page + scope[iterator + '_page'] = 1; + } + else if ( next && previous ) { + // we're in between next and previous + scope[iterator + '_page'] = parseInt(previous.match(/page=\d+/)[0].replace(/page=/,'')) + 1; + } + + // Calc the range of up to 10 pages to show + scope[iterator + '_page_range'] = new Array(); + var first = (scope[iterator + '_page'] > 5) ? scope[iterator + '_page'] - 5 : 1; + if (scope[iterator + '_page'] < 6) { + var last = (10 <= scope[iterator + '_num_pages']) ? 10 : scope[iterator + '_num_pages']; + } + else { + var last = (scope[iterator + '_page'] + 4 < scope[iterator + '_num_pages']) ? + scope[iterator + '_page'] + 4 : scope[iterator + '_num_pages']; + } + for (var i=first; i <= last; i++) { + scope[iterator + '_page_range'].push(i); + } + + } + }]) + + .factory('RelatedPaginateInit', [ 'RefreshRelated', '$cookieStore', + function(RefreshRelated, $cookieStore) { + return function(params) { + + var scope = params.scope; + var relatedSets = params.relatedSets; + var pageSize = (params.pageSize) ? params.pageSize : 10; + + for (var key in relatedSets){ + cookieSize = $cookieStore.get(relatedSets[key].iterator + '_page_size'); + scope[relatedSets[key].iterator + '_url'] = relatedSets[key].url; + if (cookieSize) { + // use the size found in session cookie, when available + scope[relatedSets[key].iterator + '_page_size'] = cookieSize; + } + else { + scope[relatedSets[key].iterator + '_page'] = 0; + scope[relatedSets[key].iterator + '_page_size'] = pageSize; + } + } + + scope.getPage = function(page, set, iterator) { + var new_url = scope[iterator + '_url'].replace(/.page\=\d+/,''); + var connect = (/\/$/.test(new_url)) ? '?' : '&'; + new_url += connect + 'page=' + page; + new_url += (scope[iterator + 'SearchParams']) ? '&' + scope[iterator + 'SearchParams'] + + '&page_size=' + scope[iterator + '_page_size' ] : 'page_size=' + scope[iterator + 'PageSize' ]; + Wait('start'); + RefreshRefresh({ scope: scope, set: set, iterator: iterator, url: new_url }); + } + + scope.pageIsActive = function(page, iterator) { + return (page == scope[iterator + '_page']) ? 'active' : ''; + } + + scope.changePageSize = function(set, iterator) { + // Called when a new page size is selected + + scope[iterator + '_page'] = 1; + var url = scope[iterator + '_url']; + + // Using the session cookie, keep track of user rows per page selection + $cookieStore.put(iterator + '_page_size', scope[iterator + '_page_size']); + + url = url.replace(/\/\?.*$/,'/'); + url += (scope[iterator + 'SearchParams']) ? '?' + scope[iterator + 'SearchParams'] + '&page_size=' + scope[iterator + '_page_size' ] : + '?page_size=' + scope[iterator + '_page_size' ]; + + RefreshRelated({ scope: scope, set: set, iterator: iterator, url: url }); + } + + } + }]) + + + .factory('PaginateInit', [ 'Refresh', '$cookieStore', 'Wait', + function(Refresh, $cookieStore, Wait) { + return function(params) { + + var scope = params.scope; + var list = params.list; + var iterator = (params.iterator) ? params.iterator : list.iterator; + var mode = (params.mode) ? params.mode : null; + var cookieSize = $cookieStore.get(iterator + '_page_size'); + + scope[iterator + '_page'] = (params.page) ? params.page : 1; + scope[iterator + '_url'] = params.url; + scope[iterator + '_mode'] = mode; + + // Set the default page size + if (cookieSize && mode != 'lookup') { + // use the size found in session cookie, when available + scope[iterator + '_page_size'] = cookieSize; + } + else { + if (params.pageSize) { + scope[iterator + '_page_size'] = params.pageSize; + } + else if (mode == 'lookup') { + scope[iterator + '_page_size'] = 5; + } + else { + scope[iterator + '_page_size'] = 20; + } + } + + scope.getPage = function(page, set, iterator) { + var new_url = scope[iterator + '_url'].replace(/.page\=\d+/,''); + var connect = (/\/$/.test(new_url)) ? '?' : '&'; + new_url += connect + 'page=' + page; + new_url += (scope[iterator + 'SearchParams']) ? '&' + scope[iterator + 'SearchParams'] + + '&page_size=' + scope[iterator + '_page_size' ] : 'page_size=' + scope[iterator + 'PageSize' ]; + Wait('start'); + Refresh({ scope: scope, set: set, iterator: iterator, url: new_url }); + } + + scope.pageIsActive = function(page, iterator) { + return (page == scope[iterator + '_page']) ? 'active' : ''; + } + + scope.changePageSize = function(set, iterator) { + // Called whenever a new page size is selected + // Using the session cookie, keep track of user rows per page selection + $cookieStore.put(iterator + '_page_size', scope[iterator + '_page_size']); + scope[iterator + '_page'] = 0; + var new_url = scope[iterator + '_url'].replace(/\?page_size\=\d+/,''); + var connect = (/\/$/.test(new_url)) ? '?' : '&'; + new_url += (scope[iterator + 'SearchParams']) ? connect + scope[iterator + 'SearchParams'] + '&page_size=' + scope[iterator + '_page_size' ] : + connect + 'page_size=' + scope[iterator + '_page_size' ]; + Wait('start'); + Refresh({ scope: scope, set: set, iterator: iterator, url: new_url }); + } + + } + }]); \ No newline at end of file diff --git a/awx/ui/static/js/helpers/inventory.js b/awx/ui/static/js/helpers/inventory.js index a335d6432a..30258b9c05 100644 --- a/awx/ui/static/js/helpers/inventory.js +++ b/awx/ui/static/js/helpers/inventory.js @@ -8,10 +8,8 @@ * */ -angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationListDefinition', - 'SearchHelper', 'PaginateHelper', 'ListGenerator', 'AuthService', - 'InventoryHelper', 'RelatedSearchHelper', 'RelatedPaginateHelper', - 'InventoryFormDefinition', 'ParseHelper' +angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationListDefinition', 'ListGenerator', 'AuthService', + 'InventoryHelper', 'InventoryFormDefinition', 'ParseHelper' ]) .factory('SaveInventory', ['InventoryForm', 'Rest', 'Alert', 'ProcessErrors', 'LookUpInit', 'OrganizationList', diff --git a/awx/ui/static/js/helpers/paginate.js b/awx/ui/static/js/helpers/paginate.js deleted file mode 100644 index 7ef1098b89..0000000000 --- a/awx/ui/static/js/helpers/paginate.js +++ /dev/null @@ -1,82 +0,0 @@ -/********************************************* - * Copyright (c) 2014 AnsibleWorks, Inc. - * - * PaginateHelper - * - * All the parts for controlling the search widget on - * related collections. - * - * PaginateInit({ - * scope: , - * list:
- * url: < - * }); - * - */ - -angular.module('PaginateHelper', ['RefreshHelper', 'ngCookies', 'Utilities']) - .factory('PaginateInit', [ 'Refresh', '$cookieStore', 'Wait', function(Refresh, $cookieStore, Wait) { - return function(params) { - - var scope = params.scope; - var list = params.list; - var iterator = (params.iterator) ? params.iterator : list.iterator; - var url = params.url; - var mode = (params.mode) ? params.mode : null; - var cookieSize = $cookieStore.get(iterator + 'PageSize'); - - if (params.page) { - scope[iterator + 'Page'] = params.page; - } - else { - scope[iterator + 'Page'] = 0; - } - - if (cookieSize && mode != 'lookup') { - // use the size found in session cookie, when available - scope[iterator + 'PageSize'] = cookieSize; - } - else { - if (params.pageSize) { - scope[iterator + 'PageSize'] = params.pageSize; - } - else if (mode == 'lookup') { - scope[iterator + 'PageSize'] = 5; - } - else { - scope[iterator + 'PageSize'] = 20; - } - } - - scope.nextSet = function(set, iterator) { - if (scope[iterator + 'NextUrl']) { - scope[iterator + 'Page']++; - Wait('start'); - Refresh({ scope: scope, set: set, iterator: iterator, url: scope[iterator + 'NextUrl'] }); - } - }; - - scope.prevSet = function(set, iterator) { - if (scope[iterator + 'PrevUrl']) { - scope[iterator + 'Page']--; - Wait('start'); - Refresh({ scope: scope, set: set, iterator: iterator, url: scope[iterator + 'PrevUrl'] }); - } - }; - - scope.changePageSize = function(set, iterator) { - // Called whenever a new page size is selected - - // Using the session cookie, keep track of user rows per page selection - $cookieStore.put(iterator + 'PageSize', scope[iterator + 'PageSize']); - - scope[iterator + 'Page'] = 0; - var new_url = url.replace(/\?page_size\=\d+/,''); - 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' ]; - Wait('start'); - Refresh({ scope: scope, set: set, iterator: iterator, url: new_url }); - } - } - }]); \ No newline at end of file diff --git a/awx/ui/static/js/helpers/refresh-related.js b/awx/ui/static/js/helpers/refresh-related.js index 47c5932325..7a53bd9d08 100644 --- a/awx/ui/static/js/helpers/refresh-related.js +++ b/awx/ui/static/js/helpers/refresh-related.js @@ -14,8 +14,9 @@ * */ -angular.module('RefreshRelatedHelper', ['RestServices', 'Utilities']) - .factory('RefreshRelated', ['ProcessErrors', 'Rest', 'Wait', function(ProcessErrors, Rest, Wait) { +angular.module('RefreshRelatedHelper', ['RestServices', 'Utilities', 'PaginationHelpers']) + .factory('RefreshRelated', ['ProcessErrors', 'Rest', 'Wait', 'PageRangeSetup', + function(ProcessErrors, Rest, Wait, PageRangeSetup) { return function(params) { var scope = params.scope; @@ -26,22 +27,15 @@ angular.module('RefreshRelatedHelper', ['RestServices', 'Utilities']) Rest.setUrl(url); Rest.get() .success( function(data, status, headers, config) { - Wait('stop'); + PageRangeSetup({ scope: scope, count: data.count, next: data.next, previous: data.previous, iterator: iterator }); scope[set] = data['results']; - scope[iterator + 'NextUrl'] = data.next; - scope[iterator + 'PrevUrl'] = data.previous; - scope[iterator + 'Count'] = data.count; - scope[iterator + 'PageCount'] = Math.ceil((data.count / scope[iterator + 'PageSize'])); - //scope[iterator + 'SearchSpin'] = false; scope[iterator + 'Loading'] = false; scope[iterator + 'HoldInput'] = false; + Wait('stop'); scope.$emit('related' + set); - if (!params.scope.$$phase) { - params.scope.$digest(); - } + }) .error ( function(data, status, headers, config) { - //scope[iterator + 'SearchSpin'] = true; ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve ' + set + '. GET returned status: ' + status }); }); diff --git a/awx/ui/static/js/helpers/refresh.js b/awx/ui/static/js/helpers/refresh.js index 19170d70e7..1efeb9fe07 100644 --- a/awx/ui/static/js/helpers/refresh.js +++ b/awx/ui/static/js/helpers/refresh.js @@ -14,34 +14,32 @@ * */ -angular.module('RefreshHelper', ['RestServices', 'Utilities']) - .factory('Refresh', ['ProcessErrors', 'Rest', 'Wait', function(ProcessErrors, Rest, Wait) { +angular.module('RefreshHelper', ['RestServices', 'Utilities', 'PaginationHelpers']) + .factory('Refresh', ['ProcessErrors', 'Rest', 'Wait', 'Empty', 'PageRangeSetup', + function(ProcessErrors, Rest, Wait, Empty, PageRangeSetup) { return function(params) { var scope = params.scope; var set = params.set; var iterator = params.iterator; var url = params.url; + scope.current_url = url; Rest.setUrl(url); Rest.get() .success( function(data, status, headers, config) { - Wait('stop'); - scope[iterator + 'NextUrl'] = data.next; - scope[iterator + 'PrevUrl'] = data.previous; - scope[iterator + 'Count'] = data.count; - scope[iterator + 'PageCount'] = Math.ceil((data.count / scope[iterator + 'PageSize'])); - //scope[iterator + 'SearchSpin'] = false; + PageRangeSetup({ scope: scope, count: data.count, next: data.next, previous: data.previous, iterator: iterator }); 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']; + window.scrollTo(0,0); + Wait('stop'); 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/related-paginate.js b/awx/ui/static/js/helpers/related-paginate.js deleted file mode 100644 index fa868962b6..0000000000 --- a/awx/ui/static/js/helpers/related-paginate.js +++ /dev/null @@ -1,67 +0,0 @@ -/********************************************* - * Copyright (c) 2014 AnsibleWorks, Inc. - * - * RelatedPaginateHelper - * - * All the parts for controlling the search widget on - * related collections. - * - * RelatedPaginateInit({ - * scope: , - * relatedSets: , - * form: - * }); - * - */ - -angular.module('RelatedPaginateHelper', ['RefreshRelatedHelper', 'ngCookies']) - .factory('RelatedPaginateInit', [ 'RefreshRelated', '$cookieStore', function(RefreshRelated, $cookieStore) { - return function(params) { - - var scope = params.scope; - var relatedSets = params.relatedSets; - var pageSize = (params.pageSize) ? params.pageSize : 10; - - for (var key in relatedSets){ - cookieSize = $cookieStore.get(relatedSets[key].iterator + 'PageSize'); - if (cookieSize) { - // use the size found in session cookie, when available - scope[relatedSets[key].iterator + 'PageSize'] = cookieSize; - } - else { - scope[relatedSets[key].iterator + 'Page'] = 0; - scope[relatedSets[key].iterator + 'PageSize'] = pageSize; - } - } - - scope.nextSet = function(set, iterator) { - scope[iterator + 'Page']++; - RefreshRelated({ scope: scope, set: set, iterator: iterator, url: scope[iterator + 'NextUrl'] }); - }; - - scope.prevSet = function(set, iterator) { - scope[iterator + 'Page']--; - RefreshRelated({ scope: scope, set: set, iterator: iterator, url: scope[iterator + 'PrevUrl'] }); - }; - - scope.changePageSize = function(set, iterator) { - // Called when a new page size is selected - var url; - scope[iterator + 'Page'] = 0; - for (var key in relatedSets) { - if (key == set) { - url = relatedSets[key].url; - break; - } - } - - // Using the session cookie, keep track of user rows per page selection - $cookieStore.put(iterator + 'PageSize', scope[iterator + 'PageSize']); - - url = url.replace(/\/\?.*$/,'/'); - url += (scope[iterator + 'SearchParams']) ? '?' + scope[iterator + 'SearchParams'] + '&page_size=' + scope[iterator + 'PageSize' ] : - '?page_size=' + scope[iterator + 'PageSize' ]; - RefreshRelated({ scope: scope, set: set, iterator: iterator, url: url }); - } - } - }]); \ No newline at end of file diff --git a/awx/ui/static/js/helpers/search.js b/awx/ui/static/js/helpers/search.js index 56f6ff4881..422636cf2f 100644 --- a/awx/ui/static/js/helpers/search.js +++ b/awx/ui/static/js/helpers/search.js @@ -16,8 +16,9 @@ */ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) - .factory('SearchInit', ['Alert', 'Rest', 'Refresh', '$location', 'GetBasePath', 'Empty', '$timeout', 'Wait', - function(Alert, Rest, Refresh, $location, GetBasePath, Empty, $timeout, Wait) { + + .factory('SearchInit', ['Alert', 'Rest', 'Refresh', '$location', 'GetBasePath', 'Empty', '$timeout', 'Wait', 'Store', + function(Alert, Rest, Refresh, $location, GetBasePath, Empty, $timeout, Wait, Store) { return function(params) { var scope = params.scope; @@ -25,14 +26,19 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) var defaultUrl = params.url; var list = params.list; var iterator = (params.iterator) ? params.iterator : list.iterator; - var sort_order; - var expected_objects=0; - var found_objects=0; - - if (scope.searchTimer) { - $timeout.cancel(scope.searchTimer); - } + var setWidgets = (params.setWidgets == false) ? false : true; + var sort_order, expected_objects=0, found_objects=0; + + var params = { + set: set, + defaultUrl: defaultUrl, + list: list, + iterator: iterator + }; + + Store('CurrentSearchParams', params); // Save in case Activity Stream widget needs to restore + function setDefaults(widget) { // Set default values var modifier = (widget == undefined || widget == 1) ? '' : widget; @@ -119,15 +125,17 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) } } - // Set default values for each search widget on the page - var widgets = (list.searchWidgets) ? list.searchWidgets : 1; - for (var i=1; i <= widgets; i++) { - var modifier = (i == 1) ? '' : i; - if ( $('#search-widget-container' + modifier) ) { - setDefaults(i); + if (setWidgets) { + // Set default values for each search widget on the page + var widgets = (list.searchWidgets) ? list.searchWidgets : 1; + for (var i=1; i <= widgets; 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, widget) { @@ -223,14 +231,13 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) if (scope.removeDoSearch) { scope.removeDoSearch(); } - scope.removeDoSearch = scope.$on('doSearch', function(e, iterator, page, load, spin) { + scope.removeDoSearch = scope.$on('doSearch', function(e, iterator, page, load) { // // 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 (scope[iterator + 'SearchParams']) { @@ -242,7 +249,7 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) } } url = url.replace(/\&\&/,'&'); - url += (scope[iterator + 'PageSize']) ? '&page_size=' + scope[iterator + 'PageSize'] : ""; + url += (scope[iterator + '_page_size']) ? '&page_size=' + scope[iterator + '_page_size'] : ""; if (page) { url += '&page=' + page; } @@ -252,38 +259,7 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) Refresh({ scope: scope, set: set, iterator: iterator, url: url }); }); - /* - if (scope.removeFoundObject) { - scope.removeFoundObject(); - } - scope.removeFoundObject = scope.$on('foundObject', function(e, iterator, page, load, spin, widget, pk) { - found_objects++; - // Add new criteria to search params - var modifier = (widget == 1) ? '' : widget; - var objs = list.fields[scope[iterator + 'SearchField' + modifier]].searchObject; - var o = (objs == 'inventories') ? 'inventory' : objs.replace(/s$/,''); - var searchFld = list.fields[scope[iterator + 'SearchField' + modifier]].searchField; - scope[iterator + 'SearchParams'] += '&' + searchFld + '__icontains=' + o; - if (!Empty(pk)) { - scope[iterator + 'SearchParams'] += '&' + searchFld + '_id__in=' + pk; - } - // Move to the next phase once all object types are processed - if (found_objects == expected_objects) { - scope.$emit('prepareSearch2', iterator, page, load, spin); - } - }); - */ - - /*if (scope.removeResultWarning) { - scope.removeResultWarning(); - } - scope.removeResultWarning = scope.$on('resultWarning', function(e, objs, length) { - // Alert the user that the # of objects was greater than 30 - var label = (objs == 'inventory') ? 'inventories' : objs.replace(/s$/,''); - Alert('Warning', 'The number of matching ' + label + ' was too large. We limited your search to the first 30.', 'alert-info'); - }); - */ - + if (scope.removePrepareSearch) { scope.removePrepareSearch(); } @@ -297,22 +273,6 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) var widgets = (list.searchWidgets) ? list.searchWidgets : 1; var modifier; - // Determine how many object values we're dealing with. - /* - expected_objects = 0; - found_objects = 0; - for (var i=1; i <= widgets; i++) { - modifier = (i == 1) ? '' : i; - scope[iterator + 'HoldInput' + modifier] = true; //Block any input until we're done. Refresh.js will flip this back. - if ($('#search-widget-container' + modifier) && - list.fields[scope[iterator + 'SearchField' + modifier]] && - list.fields[scope[iterator + 'SearchField' + modifier]].searchObject && - list.fields[scope[iterator + 'SearchField' + modifier]].searchObject !== 'all') { - expected_objects++; - } - } - */ - for (var i=1; i <= widgets; i++) { var modifier = (i == 1) ? '' : i; if ( $('#search-widget-container' + modifier) ) { @@ -329,33 +289,6 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) list.fields[scope[iterator + 'SearchField' + modifier]].searchObject + '__name__icontains=' + scope[iterator + 'SearchValue' + modifier]; - - //var objUrl = GetBasePath('base') + objs + '/?name__icontains=' + scope[iterator + 'SearchValue' + modifier]; - /* - Rest.setUrl(objUrl); - Rest.setHeader({ widget: i }); - Rest.setHeader({ object: objs }); - Rest.get() - .success( function(data, status, headers, config) { - var pk=''; - //limit result set to 30 - var len = (data.results.length > 30) ? 30 : data.results.length; - for (var j=0; j < len; j++) { - pk += "," + data.results[j].id; - } - pk = pk.replace(/^\,/,''); - scope.$emit('foundObject', iterator, page, load, spin, config.headers['widget'], pk); - if (data.results.length > 30) { - scope.$emit('resultWarning', config.headers['object'], data.results.length); - } - }) - .error( function(data, status, headers, config) { - Wait('stop'); - ProcessErrors(scope, data, status, null, - { hdr: 'Error!', msg: 'Retrieving list of ' + objs + ' where name contains: ' + scope[iterator + 'SearchValue' + modifier] + - ' GET returned status: ' + status }); - }); - */ } else { // Search value is empty @@ -363,7 +296,6 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) scope[iterator + 'SearchParams'] += '&' + list.fields[scope[iterator + 'SearchField' + modifier]].searchField + '=' + list.fields[scope[iterator + 'SearchField' + modifier]].searchObject; - //scope.$emit('foundObject', iterator, page, load, spin, i, null); } } else { @@ -374,10 +306,6 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) } } scope.$emit('prepareSearch2', iterator, page, load, spin); - //if (expected_objects == 0) { - // No search widgets contain objects - // scope.$emit('prepareSearch2', iterator, page, load, spin); - //} }); if (scope.removePrepareSearch2) { @@ -437,17 +365,6 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) 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] + '='; } @@ -491,12 +408,16 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) } } - scope.search = function(iterator, page, load, spin) { + scope.search = function(iterator, page, load) { // 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.$emit('prepareSearch', iterator, page, load, spin); + var load = (load === undefined) ? true : false; + if (load) { + scope[set] = []; + } + scope.$emit('prepareSearch', iterator, page, load); } diff --git a/awx/ui/static/js/helpers/teams.js b/awx/ui/static/js/helpers/teams.js index 00a73fe20b..8bd6e626b0 100644 --- a/awx/ui/static/js/helpers/teams.js +++ b/awx/ui/static/js/helpers/teams.js @@ -6,7 +6,7 @@ */ angular.module('TeamHelper', [ 'RestServices', 'Utilities', 'OrganizationListDefinition', - 'SearchHelper', 'PaginateHelper', 'ListGenerator' ]) + 'SearchHelper', 'PaginationHelpers', 'ListGenerator' ]) .factory('SetTeamListeners', ['Alert', 'Rest', function(Alert, Rest) { return function(params) { diff --git a/awx/ui/static/js/lists/InventoryGroups.js b/awx/ui/static/js/lists/InventoryGroups.js index 5aa136a611..f341e42b69 100644 --- a/awx/ui/static/js/lists/InventoryGroups.js +++ b/awx/ui/static/js/lists/InventoryGroups.js @@ -28,7 +28,8 @@ angular.module('InventoryGroupsDefinition', []) ngClick: "\{\{ 'showHosts(' + group.id + ',' + group.group_id + ', false)' \}\}", ngClass: "group.selected_class", hasChildren: true, - columnClass: 'col-lg-9 ellipsis', + columnClass: 'col-lg-9 col-md-9 col-sm-7 col-xs-7', + 'class': 'ellipsis', nosort: true, awDroppable: "\{\{ group.isDroppable \}\}", awDraggable: "\{\{ group.isDraggable \}\}", @@ -41,7 +42,7 @@ angular.module('InventoryGroupsDefinition', []) actions: { - columnClass: 'col-lg-3', + columnClass: 'col-lg-3 col-md-3 col-sm-5 col-xs-5', create: { mode: 'all', diff --git a/awx/ui/static/js/lists/InventoryHosts.js b/awx/ui/static/js/lists/InventoryHosts.js index 1a9760c02f..d59854352b 100644 --- a/awx/ui/static/js/lists/InventoryHosts.js +++ b/awx/ui/static/js/lists/InventoryHosts.js @@ -97,12 +97,6 @@ angular.module('InventoryHostsDefinition', []) ngHide: 'selected_tree_id == 1', //disable when 'All Hosts' selected awToolTip: "Create a new host" }, - stream: { - mode: 'all', - ngClick: "showHostActivity()", - awToolTip: "View Activity Stream", - ngShow: "user_is_superuser" - }, help: { mode: 'all', awToolTip: diff --git a/awx/ui/static/js/widgets/Stream.js b/awx/ui/static/js/widgets/Stream.js index dc2d47ccc2..5fa61f73f2 100644 --- a/awx/ui/static/js/widgets/Stream.js +++ b/awx/ui/static/js/widgets/Stream.js @@ -7,15 +7,16 @@ * */ -angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefinition', 'SearchHelper', 'PaginateHelper', +angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefinition', 'SearchHelper', 'PaginationHelpers', 'RefreshHelper', 'ListGenerator', 'StreamWidget', 'AuthService']) .factory('setStreamHeight', [ function() { return function() { // Try not to overlap footer. Because stream is positioned absolute, the parent // doesn't resize correctly when stream is loaded. - var stream = $('#stream-container'); - var height = stream.height() + 50; + var sheight = $('#stream-content').height(); + var theight = parseInt($('#tab-content-container').css('min-height').replace(/px/,'')); + var height = (theight < sheight) ? sheight : theight; $('#tab-content-container').css({ "min-height": height }); } }]) @@ -253,16 +254,20 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti .factory('Stream', ['$rootScope', '$location', 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait', 'StreamList', 'SearchInit', 'PaginateInit', 'GenerateList', 'FormatDate', 'ShowStream', 'HideStream', 'BuildDescription', 'FixUrl', 'BuildUrl', - 'ShowDetail', 'StreamBreadCrumbs', 'setStreamHeight', 'Find', + 'ShowDetail', 'StreamBreadCrumbs', 'setStreamHeight', 'Find', 'Store', function($rootScope, $location, Rest, GetBasePath, ProcessErrors, Wait, StreamList, SearchInit, PaginateInit, GenerateList, FormatDate, ShowStream, HideStream, BuildDescription, FixUrl, BuildUrl, ShowDetail, StreamBreadCrumbs, setStreamHeight, - Find) { + Find, Store) { return function(params) { var list = StreamList; var defaultUrl = GetBasePath('activity_stream'); var view = GenerateList; var base = $location.path().replace(/^\//,'').split('/')[0]; + var parent_scope = params.scope; + + // Hang onto current search params + var PreviousSearchParams = Store('SearchInitParams'); // pass in an inventory name to fix breadcrumb display var inventory_name = (params) ? params.inventory_name : null; @@ -324,11 +329,23 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti scope.closeStream = function(inUrl) { HideStream(); - if (scope.searchCleanup) + if (scope.searchCleanup) { scope.searchCleanup(); - if (inUrl) + } + // Restore prior search state + if (PreviousSearchParams) { + SearchInit({ + scope: parent_scope, + set: PreviousSearchParams.set, + list: PreviousSearchParams.list, + url: PreviousSearchParams.defaultUrl, + iterator: PreviousSearchParams.iterator, + setWidgets: false }); + } + if (inUrl) { $location.path(inUrl); } + } scope.refreshStream = function() { scope.search(list.iterator); @@ -347,14 +364,6 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti cDate = new Date(scope['activities'][i].timestamp); scope['activities'][i].timestamp = FormatDate(cDate); - // Display username - /*scope['activities'][i].user = (scope['activities'][i].summary_fields.user) ? scope['activities'][i].summary_fields.user.username : - 'system'; - if (scope['activities'][i].user !== 'system') { - // turn user into a link when not 'system' - scope['activities'][i].user = "" + - scope['activities'][i].user + ""; - }*/ if (scope['activities'][i]['summary_fields']['actor']) { scope['activities'][i]['user'] = "" + scope['activities'][i]['summary_fields']['actor']['username'] + ""; @@ -399,7 +408,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti // Give ng-repeate a chance to show the data before adjusting the page size. setTimeout(function() { setStreamHeight(); }, 500); }); - + // Initialize search and paginate pieces and load data SearchInit({ scope: scope, set: list.name, list: list, url: defaultUrl }); PaginateInit({ scope: scope, list: list, url: defaultUrl }); diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index 1e02a23c0f..0666752e3d 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -116,13 +116,16 @@ a:focus { .actions { a { font-size: 14px; - margin-right: 15px; + margin-right: 12px; + } + a:last-child { + margin-right: 0; } a:hover { cursor: pointer; } .cancel { - padding-right: 10px; + padding-right: 8px; } .dropdown .caret { border-top-color: @blue; @@ -130,6 +133,7 @@ a:focus { } #home_groups_table .actions .cancel { padding-right: 3px; } +#jobs_table .actions .cancel { padding-right: 10px; } .success-badge { color: #ffffff; @@ -534,6 +538,39 @@ legend { font-size: 11px; } +/* Pagination */ + .page-label { + margin-top: 0; + text-align: right; + } + + .pagination { + margin-top: 0; + margin-bottom: 7px; + } + + .pagination > li > a { + padding: 3px 6px; + } + + .modal-body { + .pagination { + margin-top: 15px; + margin-bottom: 0; + } + .pagination > li > a { + border: none; + padding-top: 0; + padding-bottom: 0; + } + .pagination > .active > a { + background-color: #fff; + color: #428bca; + border-color: none; + border: 1px solid #428bca; + } + } + .footer-navigation { margin: 10px 0 10px 0; } diff --git a/awx/ui/static/lib/ansible/InventoryTree.js b/awx/ui/static/lib/ansible/InventoryTree.js index da621d0a7d..7b31832698 100644 --- a/awx/ui/static/lib/ansible/InventoryTree.js +++ b/awx/ui/static/lib/ansible/InventoryTree.js @@ -136,7 +136,6 @@ angular.module('InventoryTree', ['Utilities', 'RestServices', 'GroupsHelper', 'P .success( function(data, status, headers, config) { buildAllHosts(data); buildGroups(data, 0, 0); - //console.log(groups); if (refresh) { scope.groups = groups; scope.$emit('GroupTreeRefreshed', inventory_name, groups, emit); diff --git a/awx/ui/static/lib/ansible/Utilities.js b/awx/ui/static/lib/ansible/Utilities.js index 64b8d9e5dd..dfed22aea3 100644 --- a/awx/ui/static/lib/ansible/Utilities.js +++ b/awx/ui/static/lib/ansible/Utilities.js @@ -140,7 +140,8 @@ angular.module('Utilities',['RestServices', 'Utilities']) if (form.fields[field].realName) { if (data[form.fields[field].realName]) { scope[field + '_api_error'] = data[form.fields[field]][0]; - scope[form.name + '_form'][form.fields[field].realName].$setValidity('apiError', false); + //scope[form.name + '_form'][form.fields[field].realName].$setValidity('apiError', false); + $('[name="' + form.fields[field].realName + '"]').addClass('ng-invalid'); fieldErrors = true; } } @@ -148,15 +149,16 @@ angular.module('Utilities',['RestServices', 'Utilities']) if (data[field]) { scope[form.fields[field].sourceModel + '_' + form.fields[field].sourceField + '_api_error'] = data[field][0]; - scope[form.name + '_form'][form.fields[field].sourceModel + '_' + form.fields[field].sourceField].$setValidity('apiError', false); + //scope[form.name + '_form'][form.fields[field].sourceModel + '_' + form.fields[field].sourceField].$setValidity('apiError', false); + $('[name="' + form.fields[field].sourceModel + '_' + form.fields[field].sourceField + '"]').addClass('ng-invalid'); fieldErrors = true; } } else { if (data[field]) { - console.log('setting api error: ' + form.name + '_form.' + field); scope[field + '_api_error'] = data[field][0]; - scope[form.name + '_form'][field].$setValidity('apiError', false); + //scope[form.name + '_form'][field].$setValidity('apiError', false); + $('[name="' + field + '"]').addClass('ng-invalid'); fieldErrors = true; } } @@ -523,6 +525,7 @@ angular.module('Utilities',['RestServices', 'Utilities']) } }]) + /* Empty() * * Test if a value is 'empty'. Returns true if val is null | '' | undefined. @@ -533,7 +536,40 @@ angular.module('Utilities',['RestServices', 'Utilities']) return function(val) { return (val === null || val === undefined || val === '') ? true : false; } - }]); + }]) + + + /* Store + * + * Wrapper for local storage. All local storage requests flow through here so that we can + * stringify/unstringify objects and respond to future issues in one place. For example, + * we may at some point want to only use session storage rather than local storage. We might + * want to add a test for whether or not local/session storage exists for the browser, etc. + * + * store(key,value) will store the value using the key + * + * store(key) retrieves the value of the key + * + */ + .factory('Store', ['Empty', function(Empty) { + return function(key, value) { + if (!Empty(value)) { + // Store the value + localStorage[key] = JSON.stringify(value); + } + else if (!Empty(key)) { + // Return the value + var val = localStorage[key]; + return (!Empty(val)) ? JSON.parse(val) : null; + } + } + }]) + + + + + + diff --git a/awx/ui/static/lib/ansible/form-generator.js b/awx/ui/static/lib/ansible/form-generator.js index 8c251f586a..375f1e9a99 100644 --- a/awx/ui/static/lib/ansible/form-generator.js +++ b/awx/ui/static/lib/ansible/form-generator.js @@ -237,11 +237,13 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies', 'Utilities']) if (f.sourceModel) { scope[f.sourceModel + '_' + f.sourceField] = ''; scope[f.sourceModel + '_' + f.sourceField + '_api_error'] = ''; - scope[form.name + '_form'][f.sourceModel + '_' + f.sourceField].$setValidity('apiError', true); + if (scope[form.name + '_form'][f.sourceModel + '_' + f.sourceField]) { + scope[form.name + '_form'][f.sourceModel + '_' + f.sourceField].$setValidity('apiError', true); + } } if (f.type == 'lookup' && scope[form.name + '_form'][f.sourceModel + '_' + f.sourceField]) { scope[form.name + '_form'][f.sourceModel + '_' + f.sourceField].$setPristine(); - scope[form.name + '_form'][f.sourceModel + '_' + f.sourceField].$setValidity('apiError', true); + scope[form.name + '_form'][f.sourceModel + '_' + f.sourceField].$setValidity('apiError', true); } if (scope[form.name + '_form'][fld]) { scope[form.name + '_form'][fld].$setPristine(); @@ -361,12 +363,18 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies', 'Utilities']) }, clearApiErrors: function() { - for (fld in this.form.fields) { + for (var fld in this.form.fields) { if (this.form.fields[fld].sourceModel) { this.scope[this.form.fields[fld].sourceModel + '_' + this.form.fields[fld].sourceField + '_api_error'] = ''; + $('[name="' + this.form.fields[fld].sourceModel + '_' + this.form.fields[fld].sourceField + '"]').removeClass('ng-invalid'); + } + else if (this.form.fields[fld].realName) { + this.scope[this.form.fields[fld].realName + '_api_error'] = ''; + $('[name="' + this.form.fields[fld].realName + '"]').removeClass('ng-invalid'); } else { - this.scope[fld + '_api_error'] = ''; + this.scope[fld + '_api_error'] = ''; + $('[name="' + fld + '"]').removeClass('ng-invalid'); } } if (!this.scope.$$phase) { diff --git a/awx/ui/static/lib/ansible/generator-helpers.js b/awx/ui/static/lib/ansible/generator-helpers.js index 2c13b87a98..9d29e82882 100644 --- a/awx/ui/static/lib/ansible/generator-helpers.js +++ b/awx/ui/static/lib/ansible/generator-helpers.js @@ -233,7 +233,7 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers']) var list = params['list']; var fld = params['fld']; var options = params['options']; - var field; + var html, field; if (params.field) { field = params.field; @@ -256,19 +256,6 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers']) html = ''; } - /* - html += "
\n"; - html += "\n"; - */ html += "\n"; - return html; } }) - - .factory('PaginateWidget', function() { + + .factory('PaginateWidget', [ function() { return function(params) { - var set = params.set; - var iterator = params.iterator; - var useMini = params.mini; - var mode = (params.mode) ? params.mode : null; - var html = ''; - - if (mode == 'lookup') { - html += "
\n"; - html += "\n"; - html += "\n"; - html += "\n"; - - if (mode != 'lookup') { - html += "\n"; - html += "\n"; - } - - html += "
0\" "; - html += ">Page: {{ " + iterator + "Page + 1 }} of {{ " + iterator + "PageCount }}
\n"; - html += "\n"; + var iterator = params.iterator; + var set = params.set; + var html = ''; + html += "\n"; + html += "
\n"; + html += "
\n"; + html += "\n"; + html += "
\n"; + html += "
\n"; + html += "
\n"; + html += "Page {{ " + iterator + "_page }} of {{ " + iterator + "_num_pages }} for {{ " + iterator + "_total_rows | number:0 }} " + set + '.'; + html += "
\n"; + html += "
\n"; html += "
\n"; return html; - } - }); \ No newline at end of file + }]); + + diff --git a/awx/ui/static/lib/ansible/list-generator.js b/awx/ui/static/lib/ansible/list-generator.js index d8f30762f1..bc0951eafa 100644 --- a/awx/ui/static/lib/ansible/list-generator.js +++ b/awx/ui/static/lib/ansible/list-generator.js @@ -300,7 +300,7 @@ angular.module('ListGenerator', ['GeneratorHelpers']) html += "\""; html += ">\n"; if (list.index) { - html += "{{ $index + (" + list.iterator + "Page * " + list.iterator + "PageSize) + 1 }}.\n"; + html += "{{ $index + ((" + list.iterator + "_page - 1) * " + list.iterator + "_page_size) + 1 }}.\n"; } var cnt = 2; var base = (list.base) ? list.base : list.name; @@ -391,10 +391,10 @@ angular.module('ListGenerator', ['GeneratorHelpers']) if (list.name !== 'groups') { if ( options.mode == 'lookup' || (options.id && options.id == "form-modal-body") ) { - html += PaginateWidget({ set: list.name, iterator: list.iterator, mini: true, mode: 'lookup' }); + html += PaginateWidget({ set: list.name, iterator: list.iterator }); } else { - html += PaginateWidget({ set: list.name, iterator: list.iterator, mini: true }); + html += PaginateWidget({ set: list.name, iterator: list.iterator }); } } diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index 478f1e9c69..1e1ab7be0e 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -95,11 +95,10 @@ - - + From 49901de69629d99e3b784d449f9021f5ab0b37ff Mon Sep 17 00:00:00 2001 From: James Laska Date: Tue, 28 Jan 2014 09:58:44 -0500 Subject: [PATCH 08/32] Allow customizing packer license tier --- Makefile | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index d182fa7048..e7ea085bdd 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,11 @@ DATE := $(shell date -u +%Y%m%d%H%M) VERSION=$(shell $(PYTHON) -c "from awx import __version__; print(__version__.split('-')[0])") RELEASE=$(shell $(PYTHON) -c "from awx import __version__; print(__version__.split('-')[1])") + +# Allow ami license customization +LICENSE_TIER ?= 30 +PACKER_LICENSE_FILE ?= test.json + ifneq ($(OFFICIAL),yes) BUILD=dev$(DATE) SDIST_TAR_FILE=awx-$(VERSION)-$(BUILD).tar.gz @@ -15,6 +20,7 @@ SETUP_TAR_NAME=awx-setup-$(VERSION)-$(BUILD) RPM_PKG_RELEASE=$(BUILD) DEB_BUILD_DIR=deb-build/awx-$(VERSION)-$(BUILD) DEB_PKG_RELEASE=$(VERSION)-$(BUILD) +PACKER_BUILD_OPTS=-var-file=vars-awxkeys.json -var-file=vars-nightly.json else BUILD= SDIST_TAR_FILE=awx-$(VERSION).tar.gz @@ -22,6 +28,7 @@ SETUP_TAR_NAME=awx-setup-$(VERSION) RPM_PKG_RELEASE=$(RELEASE) DEB_BUILD_DIR=deb-build/awx-$(VERSION) DEB_PKG_RELEASE=$(VERSION)-$(RELEASE) +PACKER_BUILD_OPTS=-var-file=vars-awxkeys.json -var-file=vars-release.json endif .PHONY: clean rebase push requirements requirements_pypi develop refresh \ @@ -188,12 +195,11 @@ deb: sdist @echo "awx_$(DEB_PKG_RELEASE).deb admin optional" > $(DEB_BUILD_DIR)/debian/realfiles (cd $(DEB_BUILD_DIR) && PKG_RELEASE=$(DEB_PKG_RELEASE) dpkg-buildpackage -nc -us -uc -b --changes-option="-fdebian/realfiles") -ami: - if [ "$(OFFICIAL)" = "yes" ] ; then \ - (cd packaging/ami && $(PACKER) build -var-file=vars-awxkeys.json -var-file=vars-release.json awx.json) ; \ - else \ - (cd packaging/ami && $(PACKER) build -var-file=vars-awxkeys.json -var-file=vars-nightly.json awx.json) ; \ - fi +packer_license: + @python -c "import json; fp = open('packaging/ami/license/$(PACKER_LICENSE)', 'w+'); json.dump(dict(instance_count=$(LICENSE_TIER)), fp); fp.close();" + +ami: packer_license + (cd packaging/ami && $(PACKER) build $(PACKER_BUILD_OPTS) -var "aws_license=$(PACKER_LICENSE_FILE)" awx.json) install: $(PYTHON) setup.py install egg_info -b "" From 1f6fe73a4a8fa379eca94fc072457236f27d308e Mon Sep 17 00:00:00 2001 From: James Laska Date: Tue, 28 Jan 2014 10:21:30 -0500 Subject: [PATCH 09/32] Correct incorrect Makefile variable --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e7ea085bdd..6b127cfb41 100644 --- a/Makefile +++ b/Makefile @@ -196,7 +196,7 @@ deb: sdist (cd $(DEB_BUILD_DIR) && PKG_RELEASE=$(DEB_PKG_RELEASE) dpkg-buildpackage -nc -us -uc -b --changes-option="-fdebian/realfiles") packer_license: - @python -c "import json; fp = open('packaging/ami/license/$(PACKER_LICENSE)', 'w+'); json.dump(dict(instance_count=$(LICENSE_TIER)), fp); fp.close();" + @python -c "import json; fp = open('packaging/ami/license/$(PACKER_LICENSE_FILE)', 'w+'); json.dump(dict(instance_count=$(LICENSE_TIER)), fp); fp.close();" ami: packer_license (cd packaging/ami && $(PACKER) build $(PACKER_BUILD_OPTS) -var "aws_license=$(PACKER_LICENSE_FILE)" awx.json) From be66062123263d472994f2c94b73bcd4674f0194 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Tue, 28 Jan 2014 10:58:50 -0500 Subject: [PATCH 10/32] Fixed inventory group source drop-down so that selecting 'Choose a source' behaves the same as choosing 'Manual'. Fixed AC-989. --- awx/ui/static/js/forms/Groups.js | 12 +- awx/ui/static/js/helpers/Groups.js | 194 ++++++++++---------- awx/ui/static/lib/ansible/form-generator.js | 6 +- 3 files changed, 105 insertions(+), 107 deletions(-) diff --git a/awx/ui/static/js/forms/Groups.js b/awx/ui/static/js/forms/Groups.js index 1a9691dee9..fd912a1de2 100644 --- a/awx/ui/static/js/forms/Groups.js +++ b/awx/ui/static/js/forms/Groups.js @@ -67,12 +67,12 @@ angular.module('GroupFormDefinition', []) ngChange: 'sourceChange()', addRequired: false, editRequired: false, - 'default': { label: 'Manual', value: '' }, + //'default': { label: 'Manual', value: '' }, tab: 'source' }, source_path: { label: 'Script Path', - ngShow: "source.value == 'file'", + ngShow: "source && source.value == 'file'", type: 'text', awRequiredWhen: {variable: "sourcePathRequired", init: "false" }, tab: 'source' @@ -80,7 +80,7 @@ angular.module('GroupFormDefinition', []) credential: { label: 'Cloud Credential', type: 'lookup', - ngShow: "source.value !== ''", + ngShow: "source && source.value !== ''", sourceModel: 'credential', sourceField: 'name', ngClick: 'lookUpCredential()', @@ -91,7 +91,7 @@ angular.module('GroupFormDefinition', []) source_regions: { label: 'Regions', type: 'text', - ngShow: "source.value == 'rax' || source.value == 'ec2'", + ngShow: "source && (source.value == 'rax' || source.value == 'ec2')", addRequired: false, editRequired: false, awMultiselect: 'source_region_choices', @@ -105,7 +105,7 @@ angular.module('GroupFormDefinition', []) }, source_vars: { label: 'Source Variables', - ngShow: "source.value == 'file' || source.value == 'ec2'", + ngShow: "source && (source.value == 'file' || source.value == 'ec2')", type: 'textarea', addRequired: false, editRequird: false, @@ -145,7 +145,7 @@ angular.module('GroupFormDefinition', []) checkbox_group: { label: 'Update Options', type: 'checkbox_group', - ngShow: "source.value !== '' && source.value !== null", + ngShow: "source && (source.value !== '' && source.value !== null)", tab: 'source', fields: [ diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js index 67322459ab..4624e1eb89 100644 --- a/awx/ui/static/js/helpers/Groups.js +++ b/awx/ui/static/js/helpers/Groups.js @@ -27,9 +27,13 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' Rest.options() .success( function(data, status, headers, config) { var choices = data.actions.GET.source.choices + console.log(choices); for (var i=0; i < choices.length; i++) { if (choices[i][0] !== 'file') { - scope[variable].push({ label: (choices[i][0] == "") ? 'Manual' : choices[i][1] , value: choices[i][0] }); + scope[variable].push({ + label: ( (choices[i][0] == '') ? 'Manual' : choices[i][1] ), + value: choices[i][0] + }); } } }) @@ -212,42 +216,46 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' } }]) - .factory('SourceChange', [ 'GetBasePath', 'CredentialList', 'LookUpInit', - function(GetBasePath, CredentialList, LookUpInit){ + .factory('SourceChange', [ 'GetBasePath', 'CredentialList', 'LookUpInit', 'Empty', + function(GetBasePath, CredentialList, LookUpInit, Empty){ return function(params) { var scope = params.scope; - var form = params.form; - - if (scope['source'].value == 'file') { - scope.sourcePathRequired = true; + var form = params.form; + + if (!Empty(scope['source'])) { + if (scope['source'].value == 'file') { + scope.sourcePathRequired = true; + } + else { + scope.sourcePathRequired = false; + // reset fields + scope.source_path = ''; + scope[form.name + '_form']['source_path'].$setValidity('required',true); + } + if (scope['source'].value == 'rax') { + scope['source_region_choices'] = scope['rax_regions']; + //$('#s2id_group_source_regions').select2('data', []); + $('#s2id_group_source_regions').select2('data', [{ id: 'all', text: 'All' }]); + } + else if (scope['source'].value == 'ec2') { + scope['source_region_choices'] = scope['ec2_regions']; + //$('#s2id_group_source_regions').select2('data', []); + $('#s2id_group_source_regions').select2('data', [{ id: 'all', text: 'All' }]); + } + if (scope['source'].value == 'rax' || scope['source'].value == 'ec2') { + var kind = (scope.source.value == 'rax') ? 'rax' : 'aws'; + var url = GetBasePath('credentials') + '?cloud=true&kind=' + kind; + LookUpInit({ + url: url, + scope: scope, + form: form, + list: CredentialList, + field: 'credential' + }); + } } - else { - scope.sourcePathRequired = false; - // reset fields - scope.source_path = ''; - scope[form.name + '_form']['source_path'].$setValidity('required',true); - } - if (scope['source'].value == 'rax') { - scope['source_region_choices'] = scope['rax_regions']; - //$('#s2id_group_source_regions').select2('data', []); - $('#s2id_group_source_regions').select2('data', [{ id: 'all', text: 'All' }]); - } - else if (scope['source'].value == 'ec2') { - scope['source_region_choices'] = scope['ec2_regions']; - //$('#s2id_group_source_regions').select2('data', []); - $('#s2id_group_source_regions').select2('data', [{ id: 'all', text: 'All' }]); - } - else - var kind = (scope.source.value == 'rax') ? 'rax' : 'aws'; - var url = GetBasePath('credentials') + '?cloud=true&kind=' + kind; - LookUpInit({ - url: url, - scope: scope, - form: form, - list: CredentialList, - field: 'credential' - }); + } }]) @@ -862,75 +870,65 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' var parseError = false; var saveError = false; - // Update the selector tree with new group name, descr - //SetNodeName({ scope: scope['selectedNode'], group_id: group_id, - // name: scope.name, description: scope.description }); + var data = { group: group_id, + source: ( (source && source.value) ? source.value : '' ), + source_path: scope['source_path'], + credential: scope['credential'], + overwrite: scope['overwrite'], + overwrite_vars: scope['overwrite_vars'], + update_on_launch: scope['update_on_launch'] + //update_interval: scope['update_interval'].value + }; - if (scope.source.value !== null && scope.source.value !== '') { - var data = { group: group_id, - source: scope['source'].value, - source_path: scope['source_path'], - credential: scope['credential'], - overwrite: scope['overwrite'], - overwrite_vars: scope['overwrite_vars'], - update_on_launch: scope['update_on_launch'] - //update_interval: scope['update_interval'].value - }; - - // Create a string out of selected list of regions - var regions = $('#s2id_group_source_regions').select2("data"); - var r = []; - for (var i=0; i < regions.length; i++) { - r.push(regions[i].id); + // Create a string out of selected list of regions + var regions = $('#s2id_group_source_regions').select2("data"); + var r = []; + for (var i=0; i < regions.length; i++) { + r.push(regions[i].id); + } + data['source_regions'] = r.join(); + + if (scope['source'].value == 'ec2') { + // for ec2, validate variable data + try { + if (scope.envParseType == 'json') { + var json_data = JSON.parse(scope.source_vars); //make sure JSON parses + } + else { + var json_data = jsyaml.load(scope.source_vars); //parse yaml + } + + // Make sure our JSON is actually an object + if (typeof json_data !== 'object') { + throw "failed to return an object!"; + } + + // Send JSON as a string + if ($.isEmptyObject(json_data)) { + data.source_vars = ""; + } + else { + data.source_vars = JSON.stringify(json_data, undefined, '\t'); + } } - data['source_regions'] = r.join(); - - if (scope['source'].value == 'ec2') { - // for ec2, validate variable data - try { - if (scope.envParseType == 'json') { - var json_data = JSON.parse(scope.source_vars); //make sure JSON parses - } - else { - var json_data = jsyaml.load(scope.source_vars); //parse yaml - } - - // Make sure our JSON is actually an object - if (typeof json_data !== 'object') { - throw "failed to return an object!"; - } - - // Send JSON as a string - if ($.isEmptyObject(json_data)) { - data.source_vars = ""; - } - else { - data.source_vars = JSON.stringify(json_data, undefined, '\t'); - } - } - catch(err) { - parseError = true; - scope.$emit('SaveComplete', true); - Alert("Error", "Error parsing extra variables. Parser returned: " + err); - } - } - - if (!parseError) { - Rest.setUrl(scope.source_url) - Rest.put(data) - .success( function(data, status, headers, config) { - scope.$emit('SaveComplete', false); - }) - .error( function(data, status, headers, config) { - scope.$emit('SaveComplete', true); - ProcessErrors(scope, data, status, form, - { hdr: 'Error!', msg: 'Failed to update group inventory source. PUT status: ' + status }); - }); + catch(err) { + parseError = true; + scope.$emit('SaveComplete', true); + Alert("Error", "Error parsing extra variables. Parser returned: " + err); } } - else { - // No source value - scope.$emit('SaveComplete', false); + + if (!parseError) { + Rest.setUrl(scope.source_url) + Rest.put(data) + .success( function(data, status, headers, config) { + scope.$emit('SaveComplete', false); + }) + .error( function(data, status, headers, config) { + scope.$emit('SaveComplete', true); + ProcessErrors(scope, data, status, form, + { hdr: 'Error!', msg: 'Failed to update group inventory source. PUT status: ' + status }); + }); } }); diff --git a/awx/ui/static/lib/ansible/form-generator.js b/awx/ui/static/lib/ansible/form-generator.js index 375f1e9a99..780aae9e99 100644 --- a/awx/ui/static/lib/ansible/form-generator.js +++ b/awx/ui/static/lib/ansible/form-generator.js @@ -735,7 +735,7 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies', 'Utilities']) field.awRequiredWhen.variable + "\" " : ""; html += ">\n"; html += "\n"; html += "\n"; // Add error messages @@ -1462,8 +1462,8 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies', 'Utilities']) html += "\n"; if (form.related[itm].index == undefined || form.related[itm].index !== false) { - html += "{{ $index + (" + form.related[itm].iterator + "Page * " + - form.related[itm].iterator + "PageSize) + 1 }}.\n"; + html += "{{ $index + ((" + form.related[itm].iterator + "_page - 1) * " + + form.related[itm].iterator + "_page_size) + 1 }}.\n"; } var cnt = 1; var rfield; From 049a413f7aba1183fb7650236f51185e64a08c22 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Tue, 28 Jan 2014 13:57:37 -0500 Subject: [PATCH 11/32] AC-941 don't display UI form fields until API data loads. Also fixed related set pagination issue. --- awx/ui/static/js/controllers/JobTemplates.js | 121 ++++++++++-------- awx/ui/static/js/controllers/Jobs.js | 50 ++++---- awx/ui/static/js/controllers/Teams.js | 2 +- awx/ui/static/js/forms/JobTemplates.js | 2 +- awx/ui/static/js/helpers/PaginationHelpers.js | 6 +- 5 files changed, 97 insertions(+), 84 deletions(-) diff --git a/awx/ui/static/js/controllers/JobTemplates.js b/awx/ui/static/js/controllers/JobTemplates.js index fe10b9cdf6..0fb999e30c 100644 --- a/awx/ui/static/js/controllers/JobTemplates.js +++ b/awx/ui/static/js/controllers/JobTemplates.js @@ -180,7 +180,6 @@ function JobTemplatesAdd ($scope, $rootScope, $compile, $location, $log, $routeP Wait('stop'); }) .error( function(data, status, headers, config) { - Wait('stop'); ProcessErrors(scope, data, status, form, { hdr: 'Error!', msg: 'Failed to get playbook list for ' + url +'. GET returned status: ' + status }); }); @@ -323,6 +322,7 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route var generator = GenerateForm; var form = JobTemplateForm; var scope = generator.inject(form, {mode: 'edit', related: true}); + var loadingFinishedCount = 0; scope.parseType = 'yaml'; ParseTypeChange(scope); @@ -354,12 +354,14 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route for (var i=0; i < data.length; i++) { scope.playbook_options.push(data[i]); if (data[i] == scope.playbook) { - scope['job_templates_form']['playbook'].$setValidity('required',true); + scope['job_templates_form']['playbook'].$setValidity('required',true); } } - Wait('stop'); - if (!scope.$$phase) { - scope.$digest(); + if (scope.playbook) { + scope.$emit('jobTemplateLoadFinished'); + } + else { + Wait('stop'); } }) .error( function(data, status, headers, config) { @@ -368,14 +370,12 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route ' project or make the playbooks available on the file system.', 'alert-info'); }); } - else { - Wait('stop'); - } } // Detect and alert user to potential SCM status issues var checkSCMStatus = function() { - if (!Empty(scope.project)) { + if (!Empty(scope.project)) { + Wait('start'); Rest.setUrl(GetBasePath('projects') + scope.project + '/'); Rest.get() .success( function(data, status, headers, config) { @@ -395,7 +395,7 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route ' the directory exists and file permissions are set correctly.'; break; } - + Wait('stop'); if (msg) { Alert('Waning', msg, 'alert-info'); } @@ -409,10 +409,10 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route // Register a watcher on project_name. Refresh the playbook list on change. - if (scope.selectPlaybookUnregister) { - scope.selectPlaybookUnregister(); + if (scope.watchProjectUnregister) { + scope.watchProjectUnregister(); } - scope.selectPlaybookUnregister = scope.$watch('project_name', function(oldValue, newValue) { + scope.watchProjectUnregister = scope.$watch('project_name', function(oldValue, newValue) { if (oldValue !== newValue && newValue !== '' && newValue !== null && newValue !== undefined) { scope.playbook = null; getPlaybooks(scope.project); @@ -420,6 +420,22 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route } }); + // Turn off 'Wait' after both cloud credential and playbook list come back + if (scope.removeJobTemplateLoadFinished) { + scope.removeJobTemplateLoadFinished(); + } + scope.removeJobTemplateLoadFinished = scope.$on('jobTemplateLoadFinished', function() { + loadingFinishedCount++; + if (loadingFinishedCount >= 2) { + // The initial template load finished. Now load related jobs, which + // will turn off the 'working' spinner. + for (var set in relatedSets) { + scope.search(relatedSets[set].iterator); + } + + } + }); + // Set the status/badge for each related job if (scope.removeRelatedJobs) { scope.removeRelatedJobs(); @@ -460,19 +476,18 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route field: 'cloud_credential', hdr: 'Select Cloud Credential' }); + scope.$emit('jobTemplateLoadFinished'); }); + // Retrieve each related set and populate the playbook list if (scope.jobTemplateLoadedRemove) { scope.jobTemplateLoadedRemove(); } scope.jobTemplateLoadedRemove = scope.$on('jobTemplateLoaded', function(e, related_cloud_credential) { - for (var set in relatedSets) { - scope.search(relatedSets[set].iterator); - } - getPlaybooks(scope.project); - //$('#forks-slider').slider('value',scope.forks); // align slider handle with value. + getPlaybooks(scope.project); + var dft = (scope['host_config_key'] === "" || scope['host_config_key'] === null) ? 'false' : 'true'; md5Setup({ scope: scope, @@ -488,7 +503,6 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route scope.$emit('cloudCredentialReady', data.name); }) .error( function(data, status, headers, config) { - Wait('stop'); ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to related cloud credential. GET returned status: ' + status }); }); @@ -506,48 +520,49 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route .success( function(data, status, headers, config) { LoadBreadCrumbs({ path: '/job_templates/' + id, title: data.name }); for (var fld in form.fields) { - if (fld != 'variables' && data[fld] !== null && data[fld] !== undefined) { - if (form.fields[fld].type == 'select') { - if (scope[fld + '_options'] && scope[fld + '_options'].length > 0) { - for (var i=0; i < scope[fld + '_options'].length; i++) { - if (data[fld] == scope[fld + '_options'][i].value) { - scope[fld] = scope[fld + '_options'][i]; + if (fld != 'variables' && data[fld] !== null && data[fld] !== undefined) { + if (form.fields[fld].type == 'select') { + if (scope[fld + '_options'] && scope[fld + '_options'].length > 0) { + for (var i=0; i < scope[fld + '_options'].length; i++) { + if (data[fld] == scope[fld + '_options'][i].value) { + scope[fld] = scope[fld + '_options'][i]; + } } } - } - else { + else { + scope[fld] = data[fld]; + } + } + else { scope[fld] = data[fld]; - } - } - else { - scope[fld] = data[fld]; - } - master[fld] = scope[fld]; - } - if (fld == 'variables') { - // Parse extra_vars, converting to YAML. - if ($.isEmptyObject(data.extra_vars) || data.extra_vars == "\{\}" || data.extra_vars == "null" - || data.extra_vars == "" || data.extra_vars == null) { - scope.variables = "---"; - } - else { - var json_obj = JSON.parse(data.extra_vars); - scope.variables = jsyaml.safeDump(json_obj); - } - master.variables = scope.variables; - } - if (form.fields[fld].type == 'lookup' && data.summary_fields[form.fields[fld].sourceModel]) { - scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + } + master[fld] = scope[fld]; + } + if (fld == 'variables') { + // Parse extra_vars, converting to YAML. + if ($.isEmptyObject(data.extra_vars) || data.extra_vars == "\{\}" || data.extra_vars == "null" + || data.extra_vars == "" || data.extra_vars == null) { + scope.variables = "---"; + } + else { + var json_obj = JSON.parse(data.extra_vars); + scope.variables = jsyaml.safeDump(json_obj); + } + master.variables = scope.variables; + } + if (form.fields[fld].type == 'lookup' && data.summary_fields[form.fields[fld].sourceModel]) { + scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; - master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField]; - } + } } + scope.url = data.url; var related = data.related; for (var set in form.related) { if (related[set]) { - relatedSets[set] = { url: related[set], iterator: form.related[set].iterator }; + relatedSets[set] = { url: related[set], iterator: form.related[set].iterator }; } } @@ -586,9 +601,8 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route scope.$emit('jobTemplateLoaded', data.related.cloud_credential); }) .error( function(data, status, headers, config) { - Wait('stop'); ProcessErrors(scope, data, status, form, - { hdr: 'Error!', msg: 'Failed to retrieve job template: ' + $routeParams.id + '. GET status: ' + status }); + { hdr: 'Error!', msg: 'Failed to retrieve job template: ' + $routeParams.id + '. GET status: ' + status }); }); // Save changes to the parent @@ -634,7 +648,6 @@ function JobTemplatesEdit ($scope, $rootScope, $compile, $location, $log, $route (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 }); }); diff --git a/awx/ui/static/js/controllers/Jobs.js b/awx/ui/static/js/controllers/Jobs.js index 1c806bb825..c31b1b635a 100644 --- a/awx/ui/static/js/controllers/Jobs.js +++ b/awx/ui/static/js/controllers/Jobs.js @@ -198,6 +198,7 @@ function JobsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, var master = {}; var id = $routeParams.id; var relatedSets = {}; + var loadingFinishedCount = 0; scope.job_id = id; scope.parseType = 'yaml'; @@ -213,44 +214,28 @@ function JobsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, for (var i=0; i < data.length; i++) { scope.playbook_options.push(data[i]); } + scope.$emit('jobTemplateLoadFinished'); }) .error( function(data, status, headers, config) { - //ProcessErrors(scope, data, status, form, - // { hdr: 'Error!', msg: 'Failed to get playbook list for ' + url +'. GET returned status: ' + status }); - - // Ignore the error. We get this error when the project or playbook has been deleted - + scope.$emit('jobTemplateLoadFinished'); }); } + else { + scope.$emit('jobTemplateLoadFinished'); + } } - // Register a watcher on project_name. Refresh the playbook list on change. - if (scope.selectPlaybookUnregister) { - scope.selectPlaybookUnregister(); - } - scope.selectPlaybookUnregister = scope.$watch('project_name', function(oldValue, newValue) { - if (oldValue !== newValue && newValue !== '' && newValue !== null && newValue !== undefined) { - scope.playbook = null; - getPlaybooks(scope.project); - } - }); - // Retrieve each related set and populate the playbook list if (scope.jobLoadedRemove) { scope.jobLoadedRemove(); } scope.jobLoadedRemove = scope.$on('jobLoaded', function(e, related_cloud_credential) { - scope[form.name + 'ReadOnly'] = (scope.status == 'new') ? false : true; - - // Load related sets - for (var set in relatedSets) { - scope.search(relatedSets[set].iterator); - } - // Set the playbook lookup getPlaybooks(scope.project); + scope[form.name + 'ReadOnly'] = (scope.status == 'new') ? false : true; + $('#forks-slider').slider("option", "value", scope.forks); $('#forks-slider').slider("disable"); $('input[type="checkbox"]').attr('disabled','disabled'); @@ -271,6 +256,7 @@ function JobsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, default_val: dft }); scope['callback_url'] = data.related['callback']; + scope.$emit('jobTemplateLoadFinished'); }) .error( function(data, status, headers, config) { scope['callback_url'] = '<< Job template not found >>'; @@ -282,15 +268,29 @@ function JobsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, Rest.get() .success( function(data, status, headers, config) { scope['cloud_credential_name'] = data.name; + scope.$emit('jobTemplateLoadFinished'); }) .error( function(data, status, headers, config) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to related cloud credential. GET returned status: ' + status }); }); } - - Wait('stop'); + else { + scope.$emit('jobTemplateLoadFinished'); + } + }); + // Turn off 'Wait' after both cloud credential and playbook list come back + if (scope.removeJobTemplateLoadFinished) { + scope.removeJobTemplateLoadFinished(); + } + scope.removeJobTemplateLoadFinished = scope.$on('jobTemplateLoadFinished', function() { + loadingFinishedCount++; + if (loadingFinishedCount >= 3) { + // The initial template load finished. Now load related jobs, which + // will turn off the 'working' spinner. + Wait('stop'); + } }); // Our job type options diff --git a/awx/ui/static/js/controllers/Teams.js b/awx/ui/static/js/controllers/Teams.js index 261a167f64..bc85bf9eb5 100644 --- a/awx/ui/static/js/controllers/Teams.js +++ b/awx/ui/static/js/controllers/Teams.js @@ -200,7 +200,7 @@ function TeamsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, }); // Retrieve detail record and prepopulate the form - Wait('stop'); + Wait('start'); Rest.setUrl(defaultUrl + ':id/'); Rest.get({ params: {id: id} }) .success( function(data, status, headers, config) { diff --git a/awx/ui/static/js/forms/JobTemplates.js b/awx/ui/static/js/forms/JobTemplates.js index 274a4a0d13..bfb87cf176 100644 --- a/awx/ui/static/js/forms/JobTemplates.js +++ b/awx/ui/static/js/forms/JobTemplates.js @@ -325,7 +325,7 @@ angular.module('JobTemplateFormDefinition', []) { name: "error", value: "error" }, { name: "failed", value: "failed" }, { name: "canceled", value: "canceled" } ], - badgeIcon: 'icon-job-\{\{ job.status \}\}', + badgeIcon: 'fa icon-job-\{\{ job.status \}\}', badgePlacement: 'left', badgeToolTip: "\{\{ job.statusBadgeToolTip \}\}", badgeTipPlacement: 'top', diff --git a/awx/ui/static/js/helpers/PaginationHelpers.js b/awx/ui/static/js/helpers/PaginationHelpers.js index bcab66394c..d194dd74ca 100644 --- a/awx/ui/static/js/helpers/PaginationHelpers.js +++ b/awx/ui/static/js/helpers/PaginationHelpers.js @@ -52,8 +52,8 @@ angular.module('PaginationHelpers', ['Utilities', 'RefreshHelper', 'RefreshRelat } }]) - .factory('RelatedPaginateInit', [ 'RefreshRelated', '$cookieStore', - function(RefreshRelated, $cookieStore) { + .factory('RelatedPaginateInit', [ 'RefreshRelated', '$cookieStore', 'Wait', + function(RefreshRelated, $cookieStore, Wait) { return function(params) { var scope = params.scope; @@ -80,7 +80,7 @@ angular.module('PaginationHelpers', ['Utilities', 'RefreshHelper', 'RefreshRelat new_url += (scope[iterator + 'SearchParams']) ? '&' + scope[iterator + 'SearchParams'] + '&page_size=' + scope[iterator + '_page_size' ] : 'page_size=' + scope[iterator + 'PageSize' ]; Wait('start'); - RefreshRefresh({ scope: scope, set: set, iterator: iterator, url: new_url }); + RefreshRelated({ scope: scope, set: set, iterator: iterator, url: new_url }); } scope.pageIsActive = function(page, iterator) { From f6638485b7cc7283a906a6672ffa1e308c681169 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Tue, 28 Jan 2014 15:57:00 -0500 Subject: [PATCH 12/32] AC-985 edit now edit's inventory properties via dialog- using the same edit code already written for the wrench button on inventory->groups/hosts page. --- awx/ui/static/js/controllers/Inventories.js | 16 ++++++- awx/ui/static/js/helpers/Groups.js | 1 - awx/ui/static/js/helpers/inventory.js | 48 +++++++++++++++++---- awx/ui/static/js/helpers/search.js | 32 ++++++++++---- awx/ui/static/js/lists/Inventories.js | 2 +- awx/ui/static/js/widgets/Stream.js | 3 +- 6 files changed, 80 insertions(+), 22 deletions(-) diff --git a/awx/ui/static/js/controllers/Inventories.js b/awx/ui/static/js/controllers/Inventories.js index 6547d9565c..773f340263 100644 --- a/awx/ui/static/js/controllers/Inventories.js +++ b/awx/ui/static/js/controllers/Inventories.js @@ -12,7 +12,7 @@ function InventoriesList ($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, InventoryList, GenerateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller, - ClearScope, ProcessErrors, GetBasePath, Wait, Stream) + ClearScope, ProcessErrors, GetBasePath, Wait, Stream, EditInventoryProperties) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -128,9 +128,21 @@ function InventoriesList ($scope, $rootScope, $location, $log, $routeParams, Res } }); + + if (scope.removeRefreshInventories) { + scope.removeRefreshInventories(); + } + scope.removeRefreshInventories = scope.$on('RefreshInventories', function() { + // Reflect changes after inventory properties edit completes + scope.search(list.iterator); + }); scope.showActivity = function() { Stream({ scope: scope }); } + scope.editInventoryProperties = function(inventory_id) { + EditInventoryProperties({ scope: scope, inventory_id: inventory_id }); + } + scope.addInventory = function() { $location.path($location.path() + '/add'); } @@ -186,7 +198,7 @@ function InventoriesList ($scope, $rootScope, $location, $log, $routeParams, Res InventoriesList.$inject = [ '$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'InventoryList', 'GenerateList', 'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', - 'GetBasePath', 'Wait', 'Stream' ]; + 'GetBasePath', 'Wait', 'Stream', 'EditInventoryProperties']; function InventoriesAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, InventoryForm, diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js index 4624e1eb89..bc43f150ce 100644 --- a/awx/ui/static/js/helpers/Groups.js +++ b/awx/ui/static/js/helpers/Groups.js @@ -27,7 +27,6 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' Rest.options() .success( function(data, status, headers, config) { var choices = data.actions.GET.source.choices - console.log(choices); for (var i=0; i < choices.length; i++) { if (choices[i][0] !== 'file') { scope[variable].push({ diff --git a/awx/ui/static/js/helpers/inventory.js b/awx/ui/static/js/helpers/inventory.js index 30258b9c05..ce5be78385 100644 --- a/awx/ui/static/js/helpers/inventory.js +++ b/awx/ui/static/js/helpers/inventory.js @@ -9,7 +9,7 @@ */ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationListDefinition', 'ListGenerator', 'AuthService', - 'InventoryHelper', 'InventoryFormDefinition', 'ParseHelper' + 'InventoryHelper', 'InventoryFormDefinition', 'ParseHelper', 'SearchHelper' ]) .factory('SaveInventory', ['InventoryForm', 'Rest', 'Alert', 'ProcessErrors', 'LookUpInit', 'OrganizationList', @@ -20,6 +20,7 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi // Save inventory property modifications var scope = params.scope; + var form = InventoryForm; var defaultUrl=GetBasePath('inventory'); @@ -71,7 +72,6 @@ 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 }); }); @@ -85,9 +85,9 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi .factory('EditInventoryProperties', ['InventoryForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LookUpInit', 'OrganizationList', - 'GetBasePath', 'ParseTypeChange', 'SaveInventory', 'Wait', + 'GetBasePath', 'ParseTypeChange', 'SaveInventory', 'Wait', 'Store', 'SearchInit', function(InventoryForm, GenerateForm, Rest, Alert, ProcessErrors, LookUpInit, OrganizationList, GetBasePath, ParseTypeChange, SaveInventory, - Wait) { + Wait, Store, SearchInit) { return function(params) { var parent_scope = params.scope @@ -98,6 +98,9 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi var defaultUrl=GetBasePath('inventory'); var master = {}; + // Hang onto current search params + var PreviousSearchParams = Store('CurrentSearchParams'); + form.well = false; //form.formLabelSize = 'col-lg-3'; //form.formFieldSize = 'col-lg-9'; @@ -116,8 +119,6 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi scope.formModalInfo = false; scope.formModalHeader = 'Inventory Properties'; - $('#form-modal .btn-success').removeClass('btn-none').addClass('btn-success'); - Wait('start'); Rest.setUrl(GetBasePath('inventory') + inventory_id + '/'); Rest.get() @@ -170,7 +171,6 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi }) .error( function(data, status, headers, config) { - Wait('stop'); ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get inventory: ' + inventory_id + '. GET returned: ' + status }); }); @@ -179,11 +179,41 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi scope.removeInventorySaved(); } scope.removeInventorySaved = scope.$on('InventorySaved', function() { - $('#form-modal').modal('hide'); + $('#form-modal').modal('hide'); + // Restore prior search state + if (scope.searchCleanp) { + scope.searchCleanup(); + } + SearchInit({ + scope: parent_scope, + set: PreviousSearchParams.set, + list: PreviousSearchParams.list, + url: PreviousSearchParams.defaultUrl, + iterator: PreviousSearchParams.iterator, + sort_order: PreviousSearchParams.sort_order, + setWidgets: false + }); + parent_scope.$emit('RefreshInventories'); }); + scope.cancelModal = function() { + // Restore prior search state + if (scope.searchCleanp) { + scope.searchCleanup(); + } + SearchInit({ + scope: parent_scope, + set: PreviousSearchParams.set, + list: PreviousSearchParams.list, + url: PreviousSearchParams.defaultUrl, + iterator: PreviousSearchParams.iterator, + sort_order: PreviousSearchParams.sort_order, + setWidgets: false + }); + } + scope.formModalAction = function() { - scope.inventory_id = inventory_id; + parent_scope.inventory_id = inventory_id; parent_scope.inventory_name = scope.inventory_name; SaveInventory({ scope: scope }); } diff --git a/awx/ui/static/js/helpers/search.js b/awx/ui/static/js/helpers/search.js index 422636cf2f..3d4116781d 100644 --- a/awx/ui/static/js/helpers/search.js +++ b/awx/ui/static/js/helpers/search.js @@ -27,14 +27,15 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) var list = params.list; var iterator = (params.iterator) ? params.iterator : list.iterator; var setWidgets = (params.setWidgets == false) ? false : true; - - var sort_order, expected_objects=0, found_objects=0; + var sort_order = params.sort_order || ''; + var expected_objects=0, found_objects=0; var params = { set: set, defaultUrl: defaultUrl, list: list, - iterator: iterator + iterator: iterator, + sort_order: sort_order }; Store('CurrentSearchParams', params); // Save in case Activity Stream widget needs to restore @@ -136,6 +137,17 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) } } + var params = { + set: set, + defaultUrl: defaultUrl, + list: list, + iterator: iterator, + sort_order: sort_order + }; + + Store('CurrentSearchParams', params); // Save in case Activity Stream widget needs to restore + + // Functions to handle search widget changes scope.setSearchField = function(iterator, fld, label, widget) { @@ -237,7 +249,8 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) // scope[iterator + 'Loading'] = (load == undefined || load == true) ? true : false; var url = defaultUrl; - + var connect; + //finalize and execute the query scope[iterator + 'Page'] = (page) ? parseInt(page) - 1 : 0; if (scope[iterator + 'SearchParams']) { @@ -248,14 +261,17 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) url += '&' + scope[iterator + 'SearchParams']; } } - url = url.replace(/\&\&/,'&'); - url += (scope[iterator + '_page_size']) ? '&page_size=' + scope[iterator + '_page_size'] : ""; + connect = (/\/$/.test(url)) ? '?' : '&'; + url += (scope[iterator + '_page_size']) ? connect + 'page_size=' + scope[iterator + '_page_size'] : ""; if (page) { - url += '&page=' + page; + connect = (/\/$/.test(url)) ? '?' : '&'; + url += connect + 'page=' + page; } if (scope[iterator + 'ExtraParms']) { - url += scope[iterator + 'ExtraParms']; + connect = (/\/$/.test(url)) ? '?' : '&'; + url += connect + scope[iterator + 'ExtraParms']; } + url = url.replace(/\&\&/,'&'); Refresh({ scope: scope, set: set, iterator: iterator, url: url }); }); diff --git a/awx/ui/static/js/lists/Inventories.js b/awx/ui/static/js/lists/Inventories.js index d33a32ccf5..d089c7194e 100644 --- a/awx/ui/static/js/lists/Inventories.js +++ b/awx/ui/static/js/lists/Inventories.js @@ -84,7 +84,7 @@ angular.module('InventoriesListDefinition', []) }, edit: { label: 'Edit', - ngClick: "editInventory(\{\{ inventory.id \}\})", + ngClick: 'editInventoryProperties(inventory.id)', awToolTip: 'Edit inventory', dataPlacement: 'top' }, diff --git a/awx/ui/static/js/widgets/Stream.js b/awx/ui/static/js/widgets/Stream.js index 5fa61f73f2..d13d76a525 100644 --- a/awx/ui/static/js/widgets/Stream.js +++ b/awx/ui/static/js/widgets/Stream.js @@ -267,7 +267,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti var parent_scope = params.scope; // Hang onto current search params - var PreviousSearchParams = Store('SearchInitParams'); + var PreviousSearchParams = Store('CurrentSearchParams'); // pass in an inventory name to fix breadcrumb display var inventory_name = (params) ? params.inventory_name : null; @@ -340,6 +340,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti list: PreviousSearchParams.list, url: PreviousSearchParams.defaultUrl, iterator: PreviousSearchParams.iterator, + sort_order: PreviousSearchParams.sort_order, setWidgets: false }); } if (inUrl) { From 45418626e7d9bce5df1191a42c0d74cc310c0042 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Tue, 28 Jan 2014 17:22:34 -0500 Subject: [PATCH 13/32] AC-987 job launch shows working 2x --- awx/ui/static/js/controllers/Inventories.js | 2 +- awx/ui/static/js/helpers/JobSubmission.js | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/awx/ui/static/js/controllers/Inventories.js b/awx/ui/static/js/controllers/Inventories.js index 773f340263..7960a6257d 100644 --- a/awx/ui/static/js/controllers/Inventories.js +++ b/awx/ui/static/js/controllers/Inventories.js @@ -94,7 +94,7 @@ function InventoriesList ($scope, $rootScope, $location, $log, $routeParams, Res else { // many hosts with 0 failures scope.inventories[i].failed_hosts_tip = scope.inventories[i].total_hosts + - ( (scope.inventories[i].total_hosts > 1) ? ' hosts' : ' host' ) + " with no failures. Click to view details."; + ( (scope.inventories[i].total_hosts > 1) ? ' hosts' : ' host' ) + " with no job failures. Click to view details."; scope.inventories[i].failed_hosts_link = '/#/inventories/' + scope.inventories[i].id + '/'; scope.inventories[i].failed_hosts_class = 'false'; } diff --git a/awx/ui/static/js/helpers/JobSubmission.js b/awx/ui/static/js/helpers/JobSubmission.js index 5a6e040f70..de4495d28d 100644 --- a/awx/ui/static/js/helpers/JobSubmission.js +++ b/awx/ui/static/js/helpers/JobSubmission.js @@ -230,7 +230,6 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential extra_vars: data.extra_vars }) .success( function(data, status, headers, config) { - Wait('stop'); scope.job_id = data.id; if (data.passwords_needed_to_start.length > 0) { // Passwords needed. Prompt for passwords, then start job. @@ -243,7 +242,6 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential } else { // No passwords needed, start the job! - Wait('start'); Rest.setUrl(data.related.start); Rest.post() .success( function(data, status, headers, config) { @@ -257,7 +255,6 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential } }) .error( function(data, status, headers, config) { - Wait('stop'); ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to start job. POST returned status: ' + status }); }); @@ -275,11 +272,11 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential Rest.setUrl(url); Rest.get() .success( function(data, status, headers, config) { - Wait('stop'); // Create a job record scope.credential = ''; if (data.credential == '' || data.credential == null) { // Template does not have credential, prompt for one + Wait('stop'); if (scope.credentialWatchRemove) { scope.credentialWatchRemove(); } @@ -309,7 +306,6 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential } }) .error( function(data, status, headers, config) { - Wait('stop'); ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get job template details. GET returned status: ' + status }); }); @@ -394,7 +390,6 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential } }) .error( function(data, status, headers, config) { - Wait('stop'); ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get project update details: ' + url + ' GET status: ' + status }); }); From dce52fc5ca7ecebc3d12833b8fe5f9a08c219446 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Tue, 28 Jan 2014 17:46:47 -0500 Subject: [PATCH 14/32] AC-979 using activity stream object arrays correctly. AC-980 fixed Action label on detail dialog. --- awx/ui/static/js/forms/ActivityDetail.js | 14 +----- awx/ui/static/js/widgets/Stream.js | 60 ++++++++++++++---------- 2 files changed, 36 insertions(+), 38 deletions(-) diff --git a/awx/ui/static/js/forms/ActivityDetail.js b/awx/ui/static/js/forms/ActivityDetail.js index 80e0b7df34..557e586219 100644 --- a/awx/ui/static/js/forms/ActivityDetail.js +++ b/awx/ui/static/js/forms/ActivityDetail.js @@ -23,22 +23,10 @@ angular.module('ActivityDetailDefinition', []) readonly: true }, operation: { - label: 'Operation', + label: 'Action', type: 'text', readonly: true }, - /*object1_name: { - label: '\{\{ object1 \}\}', - type: 'text', - ngHide: '!object1', - readonly: true - }, - object2_name: { - label: '\{\{ object2 \}\}', - type: 'text', - ngHide: '!object2', - readonly: true - },*/ changes: { label: 'Changes', type: 'textarea', diff --git a/awx/ui/static/js/widgets/Stream.js b/awx/ui/static/js/widgets/Stream.js index d13d76a525..e0a628db41 100644 --- a/awx/ui/static/js/widgets/Stream.js +++ b/awx/ui/static/js/widgets/Stream.js @@ -146,41 +146,51 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti descr += activity.operation; descr += (/e$/.test(activity.operation)) ? 'd ' : 'ed '; descr_nolink = descr; + + // labels var obj1 = activity.object1; var obj2 = activity.object2; + // objects + var obj1_obj = (activity.summary_fields[obj1]) ? activity.summary_fields[obj1][0] : null; + if (obj1 == obj2) { + var obj2_obj = activity.summary_fields[obj1][1]; + } + else if (activity.summary_fields[obj2]) { + var obj2_obj = activity.summary_fields[obj2][0]; + } + else { + var obj2_obj = null; + } + if (obj1 == 'user' || obj2 == 'user') { activity.summary_fields['user'][0].name = activity.summary_fields['user'][0].username; } var name; - if (activity.summary_fields[obj2] && activity.summary_fields[obj2][0].name - && !/^_delete/.test(activity.summary_fields[obj2][0].name)) { - activity.summary_fields[obj2][0]['base'] = obj2; - descr += obj2 + ' ' - + activity.summary_fields[obj2][0].name + '' + ( (activity.operation == 'disassociate') ? ' from ' : ' to ' ); - descr_nolink += obj2 + ' ' + activity.summary_fields[obj2][0].name + ( (activity.operation == 'disassociate') ? ' from ' : ' to ' ); + if (obj2_obj && obj2_obj.name && !/^_delete/.test(obj2_obj.name)) { + obj2_obj['base'] = obj2; + descr += obj2 + ' ' + + obj2_obj.name + '' + ( (activity.operation == 'disassociate') ? ' from ' : ' to ' ); + descr_nolink += obj2 + ' ' + obj2_obj.name + ( (activity.operation == 'disassociate') ? ' from ' : ' to ' ); } - else if (activity.object2) { + else if (obj2) { name = ''; - if (activity.summary_fields[obj2] && activity.summary_fields[obj2][0].name) { - name = ' ' + stripDeleted(activity.summary_fields[obj2][0].name); + if (obj2_obj && obj2_obj.name) { + name = ' ' + stripDeleted(obj2_obj.name); } - descr += activity.object2 + name + ( (activity.operation == 'disassociate') ? ' from ' : ' to ' ); - descr_nolink += activity.object2 + name + ( (activity.operation == 'disassociate') ? ' from ' : ' to ' ); + descr += obj2 + name + ( (activity.operation == 'disassociate') ? ' from ' : ' to ' ); + descr_nolink += obj2 + name + ( (activity.operation == 'disassociate') ? ' from ' : ' to ' ); } - if (activity.summary_fields[obj1] && activity.summary_fields[obj1][0].name - && !/^\_delete/.test(activity.summary_fields[obj1][0].name)) { - activity.summary_fields[obj1][0]['base'] = obj1; - descr += obj1 + ' ' - + activity.summary_fields[obj1][0].name + ''; - descr_nolink += obj1 + ' ' + activity.summary_fields[obj1][0].name; + if (obj2_obj && obj1_obj.name && !/^\_delete/.test(obj1_obj.name)) { + obj1_obj['base'] = obj1; + descr += obj1 + ' ' + obj1_obj.name + ''; + descr_nolink += obj1 + ' ' + obj1_obj.name; } - else if (activity.object1) { + else if (obj1) { name = ''; - if ( ((!(activity.summary_fields[obj1] && activity.summary_fields[obj1][0].name)) || - activity.summary_fields[obj1] && activity.summary_fields[obj1][0].name && - /^_delete/.test(activity.summary_fields[obj1][0].name)) + // find the name in changes, if needed + if ( ((!(obj1_obj && obj1_obj.name)) || obj1_obj && obj1_obj.name && /^_delete/.test(obj1_obj.name)) && (activity.changes && activity.changes.name) ) { if (typeof activity.changes.name == 'string') { name = ' ' + activity.changes.name; @@ -189,11 +199,11 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti name = ' ' + activity.changes.name[0] } } - else if (activity.summary_fields[obj1] && activity.summary_fields[obj1][0].name) { - name = ' ' + stripDeleted(activity.summary_fields[obj1][0].name); + else if (obj1_obj && obj1_obj.name) { + name = ' ' + stripDeleted(obj1_obj.name); } - descr += activity.object1 + name; - descr_nolink += activity.object1 + name; + descr += obj1 + name; + descr_nolink += obj1 + name; } activity['description'] = descr; activity['description_nolink'] = descr_nolink; From c18cb5a622eb9f8422440d1e7589e4e15054f320 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Tue, 28 Jan 2014 18:47:46 -0500 Subject: [PATCH 15/32] AC-976 add job template name to activity stream --- awx/ui/static/js/helpers/search.js | 13 ++++++++--- awx/ui/static/js/lists/Streams.js | 18 +++++++++++++++ awx/ui/static/js/widgets/Stream.js | 35 ++++++++++++++++++++++++------ 3 files changed, 56 insertions(+), 10 deletions(-) diff --git a/awx/ui/static/js/helpers/search.js b/awx/ui/static/js/helpers/search.js index 3d4116781d..b1ab7e8373 100644 --- a/awx/ui/static/js/helpers/search.js +++ b/awx/ui/static/js/helpers/search.js @@ -301,10 +301,17 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) if (scope[iterator + 'SearchValue' + modifier]) { // A search value was entered scope[iterator + 'ShowStartBtn' + modifier] = false; - scope[iterator + 'SearchParams'] += '&' + + if (list.fields[scope[iterator + 'SearchField' + modifier]].searchOnID) { + scope[iterator + 'SearchParams'] += '&' + list.fields[scope[iterator + 'SearchField' + modifier]].searchObject + - '__name__icontains=' + - scope[iterator + 'SearchValue' + modifier]; + '__id=' + scope[iterator + 'SearchValue' + modifier]; + } + else { + scope[iterator + 'SearchParams'] += '&' + + list.fields[scope[iterator + 'SearchField' + modifier]].searchObject + + '__name__icontains=' + + scope[iterator + 'SearchValue' + modifier]; + } } else { // Search value is empty diff --git a/awx/ui/static/js/lists/Streams.js b/awx/ui/static/js/lists/Streams.js index d4f47358b0..1c80032d2b 100644 --- a/awx/ui/static/js/lists/Streams.js +++ b/awx/ui/static/js/lists/Streams.js @@ -102,6 +102,15 @@ angular.module('StreamListDefinition', []) searchWidget: 2, searchField: 'object1' }, + job_search: { + label: 'Job', + searchOnly: true, + searchObject: 'job', + searchPlaceholder: 'Job id', + searchOnID: true, + searchWidget: 2, + searchField: 'object1' + }, organization_search: { label: 'Organization', searchOnly: true, @@ -169,6 +178,15 @@ angular.module('StreamListDefinition', []) searchWidget: 3, searchField: 'object2' }, + job_search3: { + label: 'Job', + searchOnly: true, + searchObject: 'job', + searchPlaceholder: 'Job id', + searchOnID: true, + searchWidget: 3, + searchField: 'object2' + }, job_template_search3: { label: 'Job Template', searchOnly: true, diff --git a/awx/ui/static/js/widgets/Stream.js b/awx/ui/static/js/widgets/Stream.js index e0a628db41..7bef535ed8 100644 --- a/awx/ui/static/js/widgets/Stream.js +++ b/awx/ui/static/js/widgets/Stream.js @@ -182,7 +182,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti descr += obj2 + name + ( (activity.operation == 'disassociate') ? ' from ' : ' to ' ); descr_nolink += obj2 + name + ( (activity.operation == 'disassociate') ? ' from ' : ' to ' ); } - if (obj2_obj && obj1_obj.name && !/^\_delete/.test(obj1_obj.name)) { + if (obj1_obj && obj1_obj.name && !/^\_delete/.test(obj1_obj.name)) { obj1_obj['base'] = obj1; descr += obj1 + ' ' + obj1_obj.name + ''; descr_nolink += obj1 + ' ' + obj1_obj.name; @@ -190,13 +190,34 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti else if (obj1) { name = ''; // find the name in changes, if needed - if ( ((!(obj1_obj && obj1_obj.name)) || obj1_obj && obj1_obj.name && /^_delete/.test(obj1_obj.name)) - && (activity.changes && activity.changes.name) ) { - if (typeof activity.changes.name == 'string') { - name = ' ' + activity.changes.name; + if ( !(obj1_obj && obj1_obj.name) || obj1_obj && obj1_obj.name && /^_delete/.test(obj1_obj.name) ) { + if (activity.changes && activity.changes.name) { + if (typeof activity.changes.name == 'string') { + name = ' ' + activity.changes.name; + } + else if (typeof activity.changes.name == 'object' && Array.isArray(activity.changes.name)) { + name = ' ' + activity.changes.name[0] + } } - else if (typeof activity.changes.name == 'object' && Array.isArray(activity.changes.name)) { - name = ' ' + activity.changes.name[0] + else if (obj1 == 'job' && obj1_obj && activity.changes && activity.changes.job_template) { + // Hack for job activity where the template name is known + if (activity.operation != 'delete') { + obj1_obj['base'] = obj1; + name = ' ' + ''+ obj1_obj.id + ' ' + activity.changes.job_template + ''; + } + else { + name = ' ' + obj1_obj.id + ' ' + activity.changes.job_template; + } + } + else if (obj1 == 'job' && obj1_obj) { + // Hack for job activity where template name not known + if (activity.operation != 'delete') { + obj1_obj['base'] = obj1; + name = ' ' + '' + obj1_obj.id + ''; + } + else { + name = ' ' + obj1_obj.id; + } } } else if (obj1_obj && obj1_obj.name) { From fd8e7fe6cb92a1af5b1bf668c0b0edab819b2106 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Wed, 29 Jan 2014 17:30:12 +0000 Subject: [PATCH 16/32] Rename package awx to ansible-tower --- Makefile | 30 +++++++++++++++--------------- docs/build_system.md | 2 +- setup.py | 8 ++++---- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Makefile b/Makefile index 6b127cfb41..c1d2ab09a1 100644 --- a/Makefile +++ b/Makefile @@ -15,20 +15,20 @@ PACKER_LICENSE_FILE ?= test.json ifneq ($(OFFICIAL),yes) BUILD=dev$(DATE) -SDIST_TAR_FILE=awx-$(VERSION)-$(BUILD).tar.gz -SETUP_TAR_NAME=awx-setup-$(VERSION)-$(BUILD) +SDIST_TAR_FILE=ansible-tower-$(VERSION)-$(BUILD).tar.gz +SETUP_TAR_NAME=ansible-tower-setup-$(VERSION)-$(BUILD) RPM_PKG_RELEASE=$(BUILD) -DEB_BUILD_DIR=deb-build/awx-$(VERSION)-$(BUILD) +DEB_BUILD_DIR=deb-build/ansible-tower-$(VERSION)-$(BUILD) DEB_PKG_RELEASE=$(VERSION)-$(BUILD) -PACKER_BUILD_OPTS=-var-file=vars-awxkeys.json -var-file=vars-nightly.json +PACKER_BUILD_OPTS=-var-file=vars-aws-keys.json -var-file=vars-nightly.json else BUILD= -SDIST_TAR_FILE=awx-$(VERSION).tar.gz -SETUP_TAR_NAME=awx-setup-$(VERSION) +SDIST_TAR_FILE=ansible-tower-$(VERSION).tar.gz +SETUP_TAR_NAME=ansible-tower-setup-$(VERSION) RPM_PKG_RELEASE=$(RELEASE) -DEB_BUILD_DIR=deb-build/awx-$(VERSION) +DEB_BUILD_DIR=deb-build/ansible-tower-$(VERSION) DEB_PKG_RELEASE=$(VERSION)-$(RELEASE) -PACKER_BUILD_OPTS=-var-file=vars-awxkeys.json -var-file=vars-release.json +PACKER_BUILD_OPTS=-var-file=vars-aws-keys.json -var-file=vars-release.json endif .PHONY: clean rebase push requirements requirements_pypi develop refresh \ @@ -168,13 +168,13 @@ sdist: clean minjs rpmtar: sdist if [ "$(OFFICIAL)" != "yes" ] ; then \ (cd dist/ && tar zxf $(SDIST_TAR_FILE)) ; \ - (cd dist/ && mv awx-$(VERSION)-$(BUILD) awx-$(VERSION)) ; \ - (cd dist/ && tar czf awx-$(VERSION).tar.gz awx-$(VERSION)) ; \ + (cd dist/ && mv ansible-tower-$(VERSION)-$(BUILD) ansible-tower-$(VERSION)) ; \ + (cd dist/ && tar czf ansible-tower-$(VERSION).tar.gz ansible-tower-$(VERSION)) ; \ fi rpm: rpmtar @mkdir -p rpm-build - @cp dist/awx-$(VERSION).tar.gz rpm-build/ + @cp dist/ansible-tower-$(VERSION).tar.gz rpm-build/ @rpmbuild --define "_topdir %(pwd)/rpm-build" \ --define "_builddir %{_topdir}" \ --define "_rpmdir %{_topdir}" \ @@ -183,23 +183,23 @@ rpm: rpmtar --define '_rpmfilename %%{NAME}-%%{VERSION}-%%{RELEASE}.%%{ARCH}.rpm' \ --define "_sourcedir %{_topdir}" \ --define "_pkgrelease $(RPM_PKG_RELEASE)" \ - -ba packaging/rpm/awx.spec + -ba packaging/rpm/ansible-tower.spec deb: sdist @mkdir -p deb-build @cp dist/$(SDIST_TAR_FILE) deb-build/ (cd deb-build && tar zxf $(SDIST_TAR_FILE)) - (cd $(DEB_BUILD_DIR) && dh_make --indep --yes -f ../$(SDIST_TAR_FILE) -p awx-$(VERSION)) + (cd $(DEB_BUILD_DIR) && dh_make --indep --yes -f ../$(SDIST_TAR_FILE) -p ansible-tower-$(VERSION)) @rm -rf $(DEB_BUILD_DIR)/debian @cp -a packaging/debian $(DEB_BUILD_DIR)/ - @echo "awx_$(DEB_PKG_RELEASE).deb admin optional" > $(DEB_BUILD_DIR)/debian/realfiles + @echo "ansible-tower_$(DEB_PKG_RELEASE).deb admin optional" > $(DEB_BUILD_DIR)/debian/realfiles (cd $(DEB_BUILD_DIR) && PKG_RELEASE=$(DEB_PKG_RELEASE) dpkg-buildpackage -nc -us -uc -b --changes-option="-fdebian/realfiles") packer_license: @python -c "import json; fp = open('packaging/ami/license/$(PACKER_LICENSE_FILE)', 'w+'); json.dump(dict(instance_count=$(LICENSE_TIER)), fp); fp.close();" ami: packer_license - (cd packaging/ami && $(PACKER) build $(PACKER_BUILD_OPTS) -var "aws_license=$(PACKER_LICENSE_FILE)" awx.json) + (cd packaging/ami && $(PACKER) build $(PACKER_BUILD_OPTS) -var "aws_license=$(PACKER_LICENSE_FILE)" ansible-tower.json) install: $(PYTHON) setup.py install egg_info -b "" diff --git a/docs/build_system.md b/docs/build_system.md index 4c961b3926..d1a80d30f9 100644 --- a/docs/build_system.md +++ b/docs/build_system.md @@ -267,7 +267,7 @@ These nightly repositories can be used by the AWX setup playbook by running the As noted above, `OFFICIAL` builds are copied out to the production server, and can be found at the following location: - http://releases.ansible.com/awx/ + http://releases.ansible.com/ansible-tower/ The AWX setup playbook will use this repo location by default. diff --git a/setup.py b/setup.py index 3d089aa5e6..7e3284d4e2 100755 --- a/setup.py +++ b/setup.py @@ -105,11 +105,11 @@ class sdist_awx(_sdist, object): super(sdist_awx, self).make_distribution() setup( - name='awx', + name='ansible-tower', version=__version__.split("-")[0], # FIXME: Should keep full version here? - author='AnsibleWorks, Inc.', - author_email='support@ansibleworks.com', - description='AWX: API, UI and Task Engine for Ansible', + author='Ansible, Inc.', + author_email='support@ansible.com', + description='ansible-tower: API, UI and Task Engine for Ansible', long_description='AWX provides a web-based user interface, REST API and ' 'task engine built on top of Ansible', license='Proprietary', From 054e1f235b5b76a6a6a3e72e409f5ce91128a70c Mon Sep 17 00:00:00 2001 From: James Laska Date: Wed, 29 Jan 2014 13:10:21 -0500 Subject: [PATCH 17/32] Fix ansible-tower deb build --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c1d2ab09a1..02d71e5e5a 100644 --- a/Makefile +++ b/Makefile @@ -192,7 +192,7 @@ deb: sdist (cd $(DEB_BUILD_DIR) && dh_make --indep --yes -f ../$(SDIST_TAR_FILE) -p ansible-tower-$(VERSION)) @rm -rf $(DEB_BUILD_DIR)/debian @cp -a packaging/debian $(DEB_BUILD_DIR)/ - @echo "ansible-tower_$(DEB_PKG_RELEASE).deb admin optional" > $(DEB_BUILD_DIR)/debian/realfiles + @echo "ansible-tower-$(DEB_PKG_RELEASE).deb admin optional" > $(DEB_BUILD_DIR)/debian/realfiles (cd $(DEB_BUILD_DIR) && PKG_RELEASE=$(DEB_PKG_RELEASE) dpkg-buildpackage -nc -us -uc -b --changes-option="-fdebian/realfiles") packer_license: From 93a0f399329a5b267419e3e018fe0f191a619f5b Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Wed, 29 Jan 2014 13:31:36 -0500 Subject: [PATCH 18/32] AC-976 now using custom javascript to apply ellipsis to long group and host names. Fixed indes.html title. --- awx/ui/static/js/controllers/Inventories.js | 13 ++- awx/ui/static/js/helpers/Groups.js | 26 ++++-- awx/ui/static/js/helpers/Hosts.js | 63 +++++++++++---- awx/ui/static/js/helpers/inventory.js | 20 +++-- awx/ui/static/js/lists/InventoryGroups.js | 1 - awx/ui/static/js/lists/InventoryHosts.js | 5 +- awx/ui/static/less/ansible-ui.less | 30 +++++-- awx/ui/static/lib/ansible/InventoryTree.js | 18 ++++- awx/ui/static/lib/ansible/Utilities.js | 79 +++++++++++++++---- awx/ui/static/lib/ansible/directives.js | 1 - .../static/lib/ansible/generator-helpers.js | 20 ++++- awx/ui/templates/ui/index.html | 4 +- 12 files changed, 216 insertions(+), 64 deletions(-) diff --git a/awx/ui/static/js/controllers/Inventories.js b/awx/ui/static/js/controllers/Inventories.js index 7960a6257d..44208ce494 100644 --- a/awx/ui/static/js/controllers/Inventories.js +++ b/awx/ui/static/js/controllers/Inventories.js @@ -316,7 +316,7 @@ function InventoriesEdit ($scope, $location, $routeParams, $compile, GenerateLis GetSyncStatusMsg, InjectHosts, HostsReload, GroupsAdd, GroupsEdit, GroupsDelete, Breadcrumbs, LoadBreadCrumbs, Empty, Rest, ProcessErrors, InventoryUpdate, Alert, ToggleChildren, ViewUpdateStatus, GroupsCancelUpdate, Find, HostsCreate, EditInventoryProperties, HostsEdit, HostsDelete, ToggleHostEnabled, CopyMoveGroup, CopyMoveHost, - Stream, GetBasePath, ShowJobSummary) + Stream, GetBasePath, ShowJobSummary, ApplyEllipsis, WatchInventoryWindowResize) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -360,6 +360,11 @@ function InventoriesEdit ($scope, $location, $routeParams, $compile, GenerateLis // Add hosts view $scope.show_failures = false; InjectHosts({ scope: $scope, inventory_id: $scope.inventory_id, tree_id: $scope.selected_tree_id, group_id: $scope.selected_group_id }); + + // As the window shrinks and expands, apply ellipsis + setTimeout(function() { ApplyEllipsis('#groups_table .group-name a'); }, 2500); //give the window time to display + WatchInventoryWindowResize(); + }); @@ -368,6 +373,8 @@ function InventoriesEdit ($scope, $location, $routeParams, $compile, GenerateLis $scope.removeGroupTreeRefreshed(); } $scope.removeGroupTreeRefreshed = $scope.$on('GroupTreeRefreshed', function(e, inventory_name, groups) { + // Reapply ellipsis to groups + setTimeout(function() { ApplyEllipsis('#groups_table .group-name a'); }, 2500); // Reselect the preveiously selected group node, causing host view to refresh. $scope.showHosts($scope.selected_tree_id, $scope.selected_group_id, false); }); @@ -525,12 +532,14 @@ function InventoriesEdit ($scope, $location, $routeParams, $compile, GenerateLis //Load tree data for the first time BuildTree({ scope: $scope, inventory_id: $scope.inventory_id, refresh: false }); + } InventoriesEdit.$inject = [ '$scope', '$location', '$routeParams', '$compile', 'GenerateList', 'ClearScope', 'InventoryGroups', 'InventoryHosts', 'BuildTree', 'Wait', 'GetSyncStatusMsg', 'InjectHosts', 'HostsReload', 'GroupsAdd', 'GroupsEdit', 'GroupsDelete', 'Breadcrumbs', 'LoadBreadCrumbs', 'Empty', 'Rest', 'ProcessErrors', 'InventoryUpdate', 'Alert', 'ToggleChildren', 'ViewUpdateStatus', 'GroupsCancelUpdate', 'Find', 'HostsCreate', 'EditInventoryProperties', 'HostsEdit', - 'HostsDelete', 'ToggleHostEnabled', 'CopyMoveGroup', 'CopyMoveHost', 'Stream', 'GetBasePath', 'ShowJobSummary' + 'HostsDelete', 'ToggleHostEnabled', 'CopyMoveGroup', 'CopyMoveHost', 'Stream', 'GetBasePath', 'ShowJobSummary', + 'ApplyEllipsis', 'WatchInventoryWindowResize' ]; diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js index bc43f150ce..073620cd2d 100644 --- a/awx/ui/static/js/helpers/Groups.js +++ b/awx/ui/static/js/helpers/Groups.js @@ -341,10 +341,10 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' .factory('GroupsAdd', ['$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'GroupForm', 'GenerateForm', 'Prompt', 'ProcessErrors', 'GetBasePath', 'ParseTypeChange', 'GroupsEdit', 'Wait', 'GetChoices', - 'GetSourceTypeOptions', 'LookUpInit', 'BuildTree', 'SourceChange', + 'GetSourceTypeOptions', 'LookUpInit', 'BuildTree', 'SourceChange', 'WatchInventoryWindowResize', function($rootScope, $location, $log, $routeParams, Rest, Alert, GroupForm, GenerateForm, Prompt, ProcessErrors, GetBasePath, ParseTypeChange, GroupsEdit, Wait, GetChoices, GetSourceTypeOptions, LookUpInit, BuildTree, - SourceChange) { + SourceChange, WatchInventoryWindowResize) { return function(params) { var inventory_id = params.inventory_id; @@ -387,11 +387,13 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' } scope.removeSaveComplete = scope.$on('SaveComplete', function(e, group_id, error) { if (!error) { - if (scope.searchCleanup) + if (scope.searchCleanup) { scope.searchCleanup(); + } scope.formModalActionDisabled = false; scope.showGroupHelp = false; //get rid of the Hint BuildTree({ scope: parent_scope, inventory_id: inventory_id, refresh: true, new_group_id: group_id }); + WatchInventoryWindowResize(); } }); @@ -484,6 +486,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' if (scope.searchCleanup) { scope.searchCleanup(); } + WatchInventoryWindowResize(); } // Save @@ -589,9 +592,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', 'LookUpInit', 'Empty', 'Wait', 'GetChoices', 'UpdateGroup', 'SourceChange', 'Find', + 'WatchInventoryWindowResize', function($rootScope, $location, $log, $routeParams, Rest, Alert, GroupForm, GenerateForm, Prompt, ProcessErrors, GetBasePath, SetNodeName, ParseTypeChange, GetSourceTypeOptions, InventoryUpdate, GetUpdateIntervalOptions, - LookUpInit, Empty, Wait, GetChoices, UpdateGroup, SourceChange, Find) { + LookUpInit, Empty, Wait, GetChoices, UpdateGroup, SourceChange, Find, WatchInventoryWindowResize) { return function(params) { var parent_scope = params.scope; @@ -855,6 +859,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' else { Wait('stop'); } + WatchInventoryWindowResize(); } }); @@ -936,6 +941,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' if (scope.searchCleanup) { scope.searchCleanup(); } + WatchInventoryWindowResize(); } // Save @@ -1076,9 +1082,9 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' .factory('ShowUpdateStatus', ['$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'GenerateForm', - 'Prompt', 'ProcessErrors', 'GetBasePath', 'FormatDate', 'InventoryStatusForm', 'Wait', 'Empty', + 'Prompt', 'ProcessErrors', 'GetBasePath', 'FormatDate', 'InventoryStatusForm', 'Wait', 'Empty', 'WatchInventoryWindowResize', function($rootScope, $location, $log, $routeParams, Rest, Alert, GenerateForm, Prompt, ProcessErrors, GetBasePath, - FormatDate, InventoryStatusForm, Wait, Empty) { + FormatDate, InventoryStatusForm, Wait, Empty, WatchInventoryWindowResize) { return function(params) { var group_name = params.group_name; @@ -1102,9 +1108,15 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' scope.formModalAction = function() { $('#form-modal').modal("hide"); if (parent_scope && parent_scope.showHosts && !Empty(tree_id)) { - if (parent_scope.selected_tree_id !== tree_id) + if (parent_scope.selected_tree_id !== tree_id) { parent_scope.showHosts(tree_id, group_id, false); + } } + WatchInventoryWindowResize(); + } + + scope.cancelModal = function() { + WatchInventoryWindowResize(); } if (scope.removeUpdateStatusReady) { diff --git a/awx/ui/static/js/helpers/Hosts.js b/awx/ui/static/js/helpers/Hosts.js index 74df35b788..beb4d89a8f 100644 --- a/awx/ui/static/js/helpers/Hosts.js +++ b/awx/ui/static/js/helpers/Hosts.js @@ -77,21 +77,21 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H return -1 * (a - b); }); title = "Recent Jobs"; - html = "\n"; + html = "
\n"; html += "\n"; html += "\n"; html += "\n"; html += "\n"; html += "\n"; for (var j=0; j < jobs.length; j++) { var job = jobs[j]; html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; + html += "\n"; + html += "\n"; + html += "\n"; html += "\n"; } html += "\n"; @@ -128,8 +128,9 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H }]) .factory('HostsReload', [ '$routeParams', 'Empty', 'InventoryHosts', 'GetBasePath', 'SearchInit', 'PaginateInit', 'Wait', - 'SetHostStatus', 'SetStatus', - function($routeParams, Empty, InventoryHosts, GetBasePath, SearchInit, PaginateInit, Wait, SetHostStatus, SetStatus) { + 'SetHostStatus', 'SetStatus', 'ApplyEllipsis', + function($routeParams, Empty, InventoryHosts, GetBasePath, SearchInit, PaginateInit, Wait, SetHostStatus, SetStatus, + ApplyEllipsis) { return function(params) { var scope = params.scope; @@ -153,6 +154,7 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H //SetHostStatus(scope.hosts[i]); } SetStatus({ scope: scope }); + setTimeout(function() { ApplyEllipsis('#hosts_table .host-name a'); }, 2500); Wait('stop'); scope.$emit('HostReloadComplete'); }); @@ -304,9 +306,9 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H .factory('HostsCreate', ['$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'HostForm', 'GenerateForm', - 'Prompt', 'ProcessErrors', 'GetBasePath', 'HostsReload', 'ParseTypeChange', 'Wait', + 'Prompt', 'ProcessErrors', 'GetBasePath', 'HostsReload', 'ParseTypeChange', 'Wait', 'WatchInventoryWindowResize', function($rootScope, $location, $log, $routeParams, Rest, Alert, HostForm, GenerateForm, Prompt, ProcessErrors, - GetBasePath, HostsReload, ParseTypeChange, Wait) { + GetBasePath, HostsReload, ParseTypeChange, Wait, WatchInventoryWindowResize) { return function(params) { var parent_scope = params.scope; @@ -349,11 +351,14 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H scope.removeHostSaveComplete = scope.$on('HostSaveComplete', function() { Wait('stop'); $('#form-modal').modal('hide'); + HostsReload({ scope: parent_scope, group_id: parent_scope.selected_group_id, tree_id: parent_scope.selected_tree_id, inventory_id: parent_scope.inventory_id }); + + WatchInventoryWindowResize(); }); // Save @@ -417,15 +422,20 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H // Defaults generator.reset(); }; - + + scope.cancelModal = function() { + WatchInventoryWindowResize(); + } + } }]) .factory('HostsEdit', ['$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'HostForm', 'GenerateForm', - 'Prompt', 'ProcessErrors', 'GetBasePath', 'HostsReload', 'ParseTypeChange', 'Wait', 'Find', 'SetStatus', + 'Prompt', 'ProcessErrors', 'GetBasePath', 'HostsReload', 'ParseTypeChange', 'Wait', 'Find', 'SetStatus', 'ApplyEllipsis', + 'WatchInventoryWindowResize', function($rootScope, $location, $log, $routeParams, Rest, Alert, HostForm, GenerateForm, Prompt, ProcessErrors, - GetBasePath, HostsReload, ParseTypeChange, Wait, Find, SetStatus) { + GetBasePath, HostsReload, ParseTypeChange, Wait, Find, SetStatus, ApplyEllipsis, WatchInventoryWindowResize) { return function(params) { var parent_scope = params.scope; @@ -513,13 +523,29 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H scope.removeSaveCompleted = scope.$on('saveCompleted', function() { // Update the name on the list var host = Find({ list: parent_scope.hosts, key: 'id', val: host_id }); + var old_name = host.name; host.name = scope.name; host.enabled = scope.enabled; host.enabled_flag = scope.enabled; SetStatus({ scope: parent_scope, host: host }); - // Close modal - Wait('stop'); - $('#form-modal').modal('hide'); + + // Update any titles attributes created by ApplyEllipsis + if (old_name) { + setTimeout(function() { + $('#hosts_table .host-name a[title="' + old_name + '"').attr('title', host.name); + ApplyEllipsis('#hosts_table .host-name a'); + // Close modal + Wait('stop'); + $('#form-modal').modal('hide'); + }, 2000); + } + else { + // Close modal + Wait('stop'); + $('#form-modal').modal('hide'); + } + // Restore ellipsis response to window resize + WatchInventoryWindowResize(); }); // Save changes to the parent @@ -580,6 +606,11 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H } scope.parseType = 'yaml'; } + + scope.cancelModal = function() { + WatchInventoryWindowResize(); + } + } }]) diff --git a/awx/ui/static/js/helpers/inventory.js b/awx/ui/static/js/helpers/inventory.js index ce5be78385..d88f5132e5 100644 --- a/awx/ui/static/js/helpers/inventory.js +++ b/awx/ui/static/js/helpers/inventory.js @@ -12,6 +12,20 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi 'InventoryHelper', 'InventoryFormDefinition', 'ParseHelper', 'SearchHelper' ]) + .factory('WatchInventoryWindowResize', ['ApplyEllipsis', function(ApplyEllipsis) { + return function() { + // Call to set or restore window resize + var timeOut; + $(window).resize(function() { + clearTimeout(timeOut); + timeOut = setTimeout(function() { + ApplyEllipsis('#groups_table .group-name a'); + ApplyEllipsis('#hosts_table .host-name a'); + }, 100); + }); + } + }]) + .factory('SaveInventory', ['InventoryForm', 'Rest', 'Alert', 'ProcessErrors', 'LookUpInit', 'OrganizationList', 'GetBasePath', 'ParseTypeChange', 'Wait', function(InventoryForm, Rest, Alert, ProcessErrors, LookUpInit, OrganizationList, GetBasePath, ParseTypeChange, Wait) { @@ -102,16 +116,12 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi var PreviousSearchParams = Store('CurrentSearchParams'); form.well = false; - //form.formLabelSize = 'col-lg-3'; - //form.formFieldSize = 'col-lg-9'; var scope = generator.inject(form, {mode: 'edit', modal: true, related: false, modal_show: false }); /* Reset form properties. Otherwise it screws up future requests of the Inventories detail page */ form.well = true; - //delete form.formLabelSize; - //delete form.formFieldSize; - + ParseTypeChange(scope,'inventory_variables', 'inventoryParseType'); scope.inventoryParseType = 'yaml'; scope.formModalActionLabel = 'Save'; diff --git a/awx/ui/static/js/lists/InventoryGroups.js b/awx/ui/static/js/lists/InventoryGroups.js index f341e42b69..79b8e9750f 100644 --- a/awx/ui/static/js/lists/InventoryGroups.js +++ b/awx/ui/static/js/lists/InventoryGroups.js @@ -29,7 +29,6 @@ angular.module('InventoryGroupsDefinition', []) ngClass: "group.selected_class", hasChildren: true, columnClass: 'col-lg-9 col-md-9 col-sm-7 col-xs-7', - 'class': 'ellipsis', nosort: true, awDroppable: "\{\{ group.isDroppable \}\}", awDraggable: "\{\{ group.isDraggable \}\}", diff --git a/awx/ui/static/js/lists/InventoryHosts.js b/awx/ui/static/js/lists/InventoryHosts.js index d59854352b..3efae93e01 100644 --- a/awx/ui/static/js/lists/InventoryHosts.js +++ b/awx/ui/static/js/lists/InventoryHosts.js @@ -26,7 +26,7 @@ angular.module('InventoryHostsDefinition', []) label: 'Hosts', ngClick: "editHost(\{\{ host.id \}\})", searchPlaceholder: "search_place_holder", - columnClass: 'col-lg-9 ellipsis', + columnClass: 'col-lg-9 col-md-9 col-sm-7 col-xs-7', dataHostId: "\{\{ host.id \}\}", dataType: "host", awDraggable: "true" @@ -91,6 +91,9 @@ angular.module('InventoryHostsDefinition', []) }, actions: { + + columnClass: 'col-lg-3 col-md-3 col-sm-5 col-xs-5', + create: { mode: 'all', ngClick: "createHost()", diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index 0666752e3d..306d5288cb 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -66,7 +66,12 @@ body.modal-open { .ellipsis { white-space: nowrap; overflow: hidden; - text-overflow: ellipsis; + text-overflow: ellipsis; +} + +.group-name { + display: inline-block; + width: 90%; } a { @@ -193,20 +198,22 @@ textarea { } /* TB tooltip overrides */ - + .popover-content, .popover-content { + width: 100%; + } .popover { - z-index: 2000; + z-index: 2000; } .tooltip { - z-index: 1050; - opacity: 1.0; + z-index: 1050; + opacity: 1.0; } - .alert { +.alert { margin-top: 15px; margin-bottom: 15px; - } +} hr { border-color: #e3e3e3; @@ -1132,6 +1139,10 @@ input[type="checkbox"].checkbox-no-label { .draggable-clone { opacity: .60; font-weight: bold; + /*z-index: 2000; + overflow: visible; + whitespace: wrap; + text-overflow: clip;*/ } .droppable-hover { @@ -1139,7 +1150,10 @@ input[type="checkbox"].checkbox-no-label { color: @info-color; padding: 6px; border: 1px solid @info-border; - border-radius: 4px; + border-radius: 4px; + /*overflow: visible; + whitespace: wrap; + text-overflow: clip;*/ } diff --git a/awx/ui/static/lib/ansible/InventoryTree.js b/awx/ui/static/lib/ansible/InventoryTree.js index 7b31832698..bafdb5f01a 100644 --- a/awx/ui/static/lib/ansible/InventoryTree.js +++ b/awx/ui/static/lib/ansible/InventoryTree.js @@ -173,18 +173,21 @@ angular.module('InventoryTree', ['Utilities', 'RestServices', 'GroupsHelper', 'P // Update a group with a set of properties - .factory('UpdateGroup', [ function() { + .factory('UpdateGroup', ['ApplyEllipsis', function(ApplyEllipsis) { return function(params) { var scope = params.scope; var group_id = params.group_id; var properties = params.properties; // object of key:value pairs to update - + var old_name; for (var i=0; i < scope.groups.length; i++) { if (scope.groups[i].group_id == group_id) { var grp = scope.groups[i]; for (var p in properties) { - scope.groups[i][p] = properties[p]; + if (p == 'name') { + old_name = scope.groups[i].name; + } + scope.groups[i][p] = properties[p]; } } if (scope.groups[i].id == scope.selected_tree_id) { @@ -194,6 +197,15 @@ angular.module('InventoryTree', ['Utilities', 'RestServices', 'GroupsHelper', 'P scope.hostSearchPlaceholder = 'Search ' + scope.groups[i].name; } } + + // Update any titles attributes created by ApplyEllipsis + if (old_name) { + setTimeout(function() { + $('#groups_table .group-name a[title="' + old_name + '"').attr('title',properties.name); + ApplyEllipsis('#groups_table .group-name a'); + }, 2500); + } + } }]) diff --git a/awx/ui/static/lib/ansible/Utilities.js b/awx/ui/static/lib/ansible/Utilities.js index dfed22aea3..4d4ba92c40 100644 --- a/awx/ui/static/lib/ansible/Utilities.js +++ b/awx/ui/static/lib/ansible/Utilities.js @@ -539,18 +539,18 @@ angular.module('Utilities',['RestServices', 'Utilities']) }]) - /* Store - * - * Wrapper for local storage. All local storage requests flow through here so that we can - * stringify/unstringify objects and respond to future issues in one place. For example, - * we may at some point want to only use session storage rather than local storage. We might - * want to add a test for whether or not local/session storage exists for the browser, etc. - * - * store(key,value) will store the value using the key - * - * store(key) retrieves the value of the key - * - */ + /* Store + * + * Wrapper for local storage. All local storage requests flow through here so that we can + * stringify/unstringify objects and respond to future issues in one place. For example, + * we may at some point want to only use session storage rather than local storage. We might + * want to add a test for whether or not local/session storage exists for the browser, etc. + * + * store(key,value) will store the value using the key + * + * store(key) retrieves the value of the key + * + */ .factory('Store', ['Empty', function(Empty) { return function(key, value) { if (!Empty(value)) { @@ -562,8 +562,59 @@ angular.module('Utilities',['RestServices', 'Utilities']) var val = localStorage[key]; return (!Empty(val)) ? JSON.parse(val) : null; } - } - }]) + + } + }]) + + /* + * + * ApplyEllipsis() + * + */ + .factory('ApplyEllipsis', [ function() { + return function(selector) { + // Add a hidden element to the DOM. We'll use this to calc the px length of + // our target text. + var tmp = $('#string-test'); + if (!tmp.length) { + $('body').append(''); + tmp = $('#string-test'); + } + // Find and process the text. + $(selector).each(function() { + var setTitle = true; + var txt; + if ($(this).attr('title')) { + txt = $(this).attr('title'); + setTitle = false; + } + else { + txt = $(this).text(); + } + tmp.text(txt); + var w = tmp.width(); //text width + var pw = $(this).parent().width(); //parent width + if (w > pw) { + // text is wider than parent width + if (setTitle) { + // Save the original text in the title + $(this).attr('title',txt); + } + var cw = w / txt.length; // px width per character + var df = w - pw; // difference in px + txt = txt.substr(0, txt.length - (Math.ceil(df / cw) + 3)); + $(this).text(txt + '...'); + } + if (pw > w && !setTitle) { + // the parent has expanded and we previously set the title text + var txt = $(this).attr('title'); + $(this).text(txt); + } + }); + + } + }]); + diff --git a/awx/ui/static/lib/ansible/directives.js b/awx/ui/static/lib/ansible/directives.js index 0225526dfc..004e5deae3 100644 --- a/awx/ui/static/lib/ansible/directives.js +++ b/awx/ui/static/lib/ansible/directives.js @@ -582,7 +582,6 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job found = true; break; } - } return (found) ? false : true; } diff --git a/awx/ui/static/lib/ansible/generator-helpers.js b/awx/ui/static/lib/ansible/generator-helpers.js index 9d29e82882..2253508c2a 100644 --- a/awx/ui/static/lib/ansible/generator-helpers.js +++ b/awx/ui/static/lib/ansible/generator-helpers.js @@ -471,9 +471,16 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers']) // Add collapse/expand icon --used on job_events page if (list['hasChildren'] && field.hasChildren) { - html += " " + - " "; - //ng-show=\"'\{\{ " + list.iterator + ".related.children \}\}' !== ''\" + html += " " + + " "; + //ng-show=\"'\{\{ " + list.iterator + ".related.children \}\}' !== ''\" + } + + if (list.name == 'groups') { + html += "
"; + } + if (list.name == 'hosts') { + html += "
"; } // Start the Link @@ -550,9 +557,14 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers']) && options.mode != 'lookup' && options.mode != 'select' && !field.noLink && !field.ngBindHtml ) { html += ""; } + + if (list.name == 'hosts' || list.name == 'groups') { + html += "
"; + } + // close ngShow html += (field.ngShow) ? "" : ""; - + // Specific to Job Events page -showing event detail/results html += (field.appendHTML) ? "
- AnsibleWorks AWX + Ansible Tower @@ -244,7 +244,7 @@
\n"; html += "\n"; html += "\n"; html += "\n"; @@ -90,8 +90,8 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H var job = jobs[j]; html += "\n"; html += "\n"; - html += "\n"; - html += "\n"; + html += "\n"; + html += "\n"; html += "\n"; } html += "\n"; diff --git a/awx/ui/static/js/helpers/Jobs.js b/awx/ui/static/js/helpers/Jobs.js index af723d1868..6e5ebd7426 100644 --- a/awx/ui/static/js/helpers/Jobs.js +++ b/awx/ui/static/js/helpers/Jobs.js @@ -7,7 +7,7 @@ * */ -angular.module('JobsHelper', ['Utilities', 'FormGenerator', 'JobSummaryDefinition']) +angular.module('JobsHelper', ['Utilities', 'FormGenerator', 'JobSummaryDefinition', 'InventoryHelper']) .factory('JobStatusToolTip', [ function() { return function(status) { @@ -41,7 +41,8 @@ angular.module('JobsHelper', ['Utilities', 'FormGenerator', 'JobSummaryDefinitio }]) .factory('ShowJobSummary', ['Rest', 'Wait', 'GetBasePath', 'FormatDate', 'ProcessErrors', 'GenerateForm', 'JobSummary', - function(Rest, Wait, GetBasePath, FormatDate, ProcessErrors, GenerateForm, JobSummary) { + 'WatchInventoryWindowResize', + function(Rest, Wait, GetBasePath, FormatDate, ProcessErrors, GenerateForm, JobSummary, WatchInventoryWindowResize) { return function(params) { // Display status info in a modal dialog- called from inventory edit page @@ -109,6 +110,7 @@ angular.module('JobsHelper', ['Utilities', 'FormGenerator', 'JobSummaryDefinitio }); $('#status-modal-dialog').dialog('destroy'); $('#inventory-modal-container').empty(); + WatchInventoryWindowResize(); }, open: function(e, ui) { Wait('stop'); diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index 306d5288cb..80c1e8a33a 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -379,7 +379,7 @@ dd { .navbar>.container .navbar-brand { margin-left: 0; - margin-top: 10px; + margin-top: 11px; } /* Using inline-block rather than block keeps From fee658e950ee8cd3d746fc168eb3f584193fc410 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Wed, 29 Jan 2014 13:39:40 -0500 Subject: [PATCH 20/32] AC-992 Fix inventory import tests. --- awx/main/tests/commands.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/awx/main/tests/commands.py b/awx/main/tests/commands.py index e773ed3cbf..05dd37c665 100644 --- a/awx/main/tests/commands.py +++ b/awx/main/tests/commands.py @@ -462,12 +462,12 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest): self.assertEqual(inventory_source.inventory_updates.count(), 1) inventory_update = inventory_source.inventory_updates.all()[0] self.assertEqual(inventory_update.status, 'successful') - for host in inventory.hosts.all(): + for host in inventory.hosts.filter(active=True): if host.pk in (except_host_pks or []): continue source_pks = host.inventory_sources.values_list('pk', flat=True) self.assertTrue(inventory_source.pk in source_pks) - for group in inventory.groups.all(): + for group in inventory.groups.filter(active=True): if group.pk in (except_group_pks or []): continue source_pks = group.inventory_sources.values_list('pk', flat=True) @@ -619,20 +619,20 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest): 'lbservers']) if overwrite: expected_group_names.remove('lbservers') - group_names = set(new_inv.groups.values_list('name', flat=True)) + group_names = set(new_inv.groups.filter(active=True).values_list('name', flat=True)) self.assertEqual(expected_group_names, group_names) expected_host_names = set(['web1.example.com', 'web2.example.com', 'web3.example.com', 'db1.example.com', 'db2.example.com', 'lb.example.com']) if overwrite: expected_host_names.remove('lb.example.com') - host_names = set(new_inv.hosts.values_list('name', flat=True)) + host_names = set(new_inv.hosts.filter(active=True).values_list('name', flat=True)) self.assertEqual(expected_host_names, host_names) expected_inv_vars = {'vara': 'A', 'varc': 'C'} if overwrite or overwrite_vars: expected_inv_vars.pop('varc') self.assertEqual(new_inv.variables_dict, expected_inv_vars) - for host in new_inv.hosts.all(): + for host in new_inv.hosts.filter(active=True): if host.name == 'web1.example.com': self.assertEqual(host.variables_dict, {'ansible_ssh_host': 'w1.example.net'}) @@ -642,35 +642,35 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest): self.assertEqual(host.variables_dict, {'lbvar': 'ni!'}) else: self.assertEqual(host.variables_dict, {}) - for group in new_inv.groups.all(): + for group in new_inv.groups.filter(active=True): if group.name == 'servers': expected_vars = {'varb': 'B', 'vard': 'D'} if overwrite or overwrite_vars: expected_vars.pop('vard') self.assertEqual(group.variables_dict, expected_vars) - children = set(group.children.values_list('name', flat=True)) + children = set(group.children.filter(active=True).values_list('name', flat=True)) expected_children = set(['dbservers', 'webservers', 'lbservers']) if overwrite: expected_children.remove('lbservers') self.assertEqual(children, expected_children) - self.assertEqual(group.hosts.count(), 0) + self.assertEqual(group.hosts.filter(active=True).count(), 0) elif group.name == 'dbservers': self.assertEqual(group.variables_dict, {'dbvar': 'ugh'}) - self.assertEqual(group.children.count(), 0) - hosts = set(group.hosts.values_list('name', flat=True)) + self.assertEqual(group.children.filter(active=True).count(), 0) + hosts = set(group.hosts.filter(active=True).values_list('name', flat=True)) host_names = set(['db1.example.com','db2.example.com']) self.assertEqual(hosts, host_names) elif group.name == 'webservers': self.assertEqual(group.variables_dict, {'webvar': 'blah'}) - self.assertEqual(group.children.count(), 0) - hosts = set(group.hosts.values_list('name', flat=True)) + self.assertEqual(group.children.filter(active=True).count(), 0) + hosts = set(group.hosts.filter(active=True).values_list('name', flat=True)) host_names = set(['web1.example.com','web2.example.com', 'web3.example.com']) self.assertEqual(hosts, host_names) elif group.name == 'lbservers': self.assertEqual(group.variables_dict, {}) - self.assertEqual(group.children.count(), 0) - hosts = set(group.hosts.values_list('name', flat=True)) + self.assertEqual(group.children.filter(active=True).count(), 0) + hosts = set(group.hosts.filter(active=True).values_list('name', flat=True)) host_names = set(['lb.example.com']) self.assertEqual(hosts, host_names) if overwrite: From b6f623229be27084ff0bf66cffafb98441fa4928 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Wed, 29 Jan 2014 13:49:29 -0500 Subject: [PATCH 21/32] Update make develop command, version check after package rename. --- Makefile | 4 +++- awx/main/utils.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 02d71e5e5a..7863a6a6a6 100644 --- a/Makefile +++ b/Makefile @@ -72,12 +72,14 @@ requirements_pypi: sudo pip install -r requirements/dev.txt; \ fi -# "Install" awx package in development mode. Creates link to working +# "Install" ansible-tower package in development mode. Creates link to working # copy in site-packages and installs awx-manage command. develop: @if [ "$(VIRTUAL_ENV)" ]; then \ + pip uninstall -y awx; \ $(PYTHON) setup.py develop; \ else \ + sudo pip uninstall -y awx; \ sudo $(PYTHON) setup.py develop; \ fi diff --git a/awx/main/utils.py b/awx/main/utils.py index 88c11646f0..0ff157138e 100644 --- a/awx/main/utils.py +++ b/awx/main/utils.py @@ -77,7 +77,7 @@ def get_ansible_version(): def get_awx_version(): ''' - Return AWX version as reported by setuptools. + Return Ansible Tower version as reported by setuptools. ''' from awx import __version__ try: From 48f81c5be7a43792c6b970b5515fa0ac839ff965 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Wed, 29 Jan 2014 14:01:34 -0500 Subject: [PATCH 22/32] Fixing store references to point to http://www.ansible.com/ansible-pricing --- awx/ui/static/js/helpers/Access.js | 2 +- awx/ui/static/lib/ansible/license.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/static/js/helpers/Access.js b/awx/ui/static/js/helpers/Access.js index 26202a48e9..2c3d00bf29 100644 --- a/awx/ui/static/js/helpers/Access.js +++ b/awx/ui/static/js/helpers/Access.js @@ -50,7 +50,7 @@ angular.module('AccessHelper', ['RestServices', 'Utilities', 'ngCookies']) var license = $cookieStore.get('license'); var purchase_msg = '

To purchase a license or extend an existing license ' + - 'visit the Ansible online store, ' + + 'visit the Ansible online store, ' + 'or visit support.ansible.com for assistance.

'; if (license && !Authorization.licenseTested()) { diff --git a/awx/ui/static/lib/ansible/license.js b/awx/ui/static/lib/ansible/license.js index cacba6b0ce..ebf08eec95 100644 --- a/awx/ui/static/lib/ansible/license.js +++ b/awx/ui/static/lib/ansible/license.js @@ -131,7 +131,7 @@ angular.module('License', ['RestServices', 'Utilities', 'FormGenerator', 'Prompt Prompt({ hdr: 'Tower Licensing', body: "

Ansible Tower licenses can be purchased or extended by visiting " + + "href=\"http://www.ansible.com/ansible-pricing\" target=\"_blank\">" + "the Ansible online store. Would you like to purchase or extend your license now?

", 'class': 'btn-primary', action: function() { From 2e45ad1a639c79d59f8149dcbdf33fe581341586 Mon Sep 17 00:00:00 2001 From: James Laska Date: Wed, 29 Jan 2014 14:04:31 -0500 Subject: [PATCH 23/32] Remove 'awx-' prefix when reporting version --- awx/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/__init__.py b/awx/__init__.py index 6a23bcf9b0..5633508baa 100644 --- a/awx/__init__.py +++ b/awx/__init__.py @@ -71,6 +71,6 @@ def manage(): # Now run the command (or display the version). from django.core.management import execute_from_command_line if len(sys.argv) >= 2 and sys.argv[1] in ('version', '--version'): - sys.stdout.write('awx-%s\n' % __version__) + sys.stdout.write('%s\n' % __version__) else: execute_from_command_line(sys.argv) From 5c076529c0d4e2c8f9d77e2b983c3e91f56ed08b Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 27 Jan 2014 11:37:18 -0500 Subject: [PATCH 24/32] Initial work towards the celery refactor... adjusting logic to allow building a worker chain... temporarily relax requirements on status checks --- awx/main/models/jobs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 5deabe4f4d..7423b61290 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -343,7 +343,6 @@ class Job(CommonTask): opts = dict([(field, kwargs.get(field, '')) for field in needed]) if not all(opts.values()): return False - # TODO: This is temporary to allow a dependent task to continue self.status = 'waiting' self.save(update_fields=['status']) transaction.commit() From db14daf5e5951a0fee6b746b39db261f24844f70 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 27 Jan 2014 12:20:21 -0500 Subject: [PATCH 25/32] Leave another TODO on the job runner, fix a misspelling on the project update hook --- awx/main/models/jobs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 7423b61290..5deabe4f4d 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -343,6 +343,7 @@ class Job(CommonTask): opts = dict([(field, kwargs.get(field, '')) for field in needed]) if not all(opts.values()): return False + # TODO: This is temporary to allow a dependent task to continue self.status = 'waiting' self.save(update_fields=['status']) transaction.commit() From 6c81d497de1f58e15d5cad9fd7b421d815b6eb6b Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 29 Jan 2014 15:11:55 -0500 Subject: [PATCH 26/32] Fix some bugs and show more error detail on a current task when a previous task fails --- awx/main/models/jobs.py | 4 ++-- awx/main/tasks.py | 18 +++++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 5deabe4f4d..3137b0848d 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -368,7 +368,7 @@ class Job(CommonTask): if is_qs.count(): for inventory_source in is_qs: inventory_update_details = inventory_source.update_signature() - if not inventory_update: + if not inventory_update_details: # TODO: Set error here pass else: @@ -381,7 +381,7 @@ class Job(CommonTask): run_tasks.append(runnable_tasks[idx]['sig'].set(link_error=handle_work_error.s(subtasks=dependent_tasks))) run_tasks.append(task_class().si(self.pk, **opts).set(link_error=handle_work_error.s(subtasks=[thisjob]))) print runnable_tasks - res = chain(runnable_tasks)() + res = chain(run_tasks)() return True class JobHostSummary(BaseModel): diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 04227572f9..482622d235 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -44,21 +44,33 @@ logger = logging.getLogger('awx.main.tasks') @task(bind=True) def handle_work_error(self, task_id, subtasks=None): print('Executing error task id %s, subtasks: %s' % (str(self.request.id), str(subtasks))) + first_task = None + first_task_type = '' + first_task_name = '' if subtasks is not None: for each_task in subtasks: + instance_name = '' if each_task['type'] == 'project_update': instance = ProjectUpdate.objects.get(id=each_task['id']) + instance_name = instance.project.name elif each_task['type'] == 'inventory_update': instance = InventoryUpdate.objects.get(id=each_task['id']) - elif each_task['type': 'job']: + instance_name = instance.inventory_source.inventory.name + elif each_task['type'] == 'job': instance = Job.objects.get(id=each_task['id']) + instance_name = instance.job_template.name else: # Unknown task type break - if instance.celery_task_id != instance.celery_task_id: + if first_task is None: + first_task = instance + first_task_type = each_task['type'] + first_task_name = instance_name + if instance.celery_task_id != task_id: instance.status = 'failed' instance.failed = True - instance.result_traceback = "Previous Task Failed: %s" % str(subtasks) + instance.result_traceback = "Previous Task Failed: %s for %s with celery task id: %s" % \ + (first_task_type, first_task_name, task_id) instance.save() class BaseTask(Task): From 6ff98d87e966315bb72fd6bdd1910a70416e11d4 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 30 Jan 2014 01:04:17 -0500 Subject: [PATCH 27/32] Fix merge spacing from conflicted merge --- awx/ui/static/js/controllers/Inventories.js | 2 +- awx/ui/static/js/lists/InventoryHosts.js | 2 +- awx/ui/static/js/widgets/Stream.js | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/awx/ui/static/js/controllers/Inventories.js b/awx/ui/static/js/controllers/Inventories.js index 65aa7dc7a5..e976465537 100644 --- a/awx/ui/static/js/controllers/Inventories.js +++ b/awx/ui/static/js/controllers/Inventories.js @@ -521,7 +521,7 @@ function InventoriesEdit ($scope, $location, $routeParams, $compile, GenerateLis } $scope.showGroupActivity = function() { - var url, title, group; + var url, title, group; if ($scope.selected_group_id) { group = Find({ list: $scope.groups, key: 'id', val: $scope.selected_tree_id }); url = GetBasePath('activity_stream') + '?group__id=' + $scope.selected_group_id; diff --git a/awx/ui/static/js/lists/InventoryHosts.js b/awx/ui/static/js/lists/InventoryHosts.js index a299760030..c0f9ad40b0 100644 --- a/awx/ui/static/js/lists/InventoryHosts.js +++ b/awx/ui/static/js/lists/InventoryHosts.js @@ -100,7 +100,7 @@ angular.module('InventoryHostsDefinition', []) ngHide: 'selected_tree_id == 1', //disable when 'All Hosts' selected awToolTip: "Create a new host" }, - stream: { + stream: { ngClick: "showHostActivity()", awToolTip: "View Activity Stream", mode: 'all', diff --git a/awx/ui/static/js/widgets/Stream.js b/awx/ui/static/js/widgets/Stream.js index b4d398505f..fe34fc04d4 100644 --- a/awx/ui/static/js/widgets/Stream.js +++ b/awx/ui/static/js/widgets/Stream.js @@ -195,7 +195,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti if (activity.changes && activity.changes.name) { if (typeof activity.changes.name == 'string') { name = ' ' + activity.changes.name; - name_nolink = name; + name_nolink = name; } else if (typeof activity.changes.name == 'object' && Array.isArray(activity.changes.name)) { name = ' ' + activity.changes.name[0]; @@ -219,7 +219,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti if (activity.operation != 'delete') { obj1_obj['base'] = obj1; name = ' ' + '' + obj1_obj.id + ''; - name_nolink = ' ' + obj1_obj.id; + name_nolink = ' ' + obj1_obj.id; } else { name = ' ' + obj1_obj.id; @@ -229,7 +229,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti } else if (obj1_obj && obj1_obj.name) { name = ' ' + stripDeleted(obj1_obj.name); - name_nolink = name; + name_nolink = name; } descr += obj1 + name; descr_nolink += obj1 + name_nolink; From 6afc584a4f382acffaed6b9ccae0e39ebff0c653 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 30 Jan 2014 01:19:49 -0500 Subject: [PATCH 28/32] Check for invalid tasks and mark created tasks as failed when constructing task chains --- awx/main/models/jobs.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 3137b0848d..a2ca766ecd 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -352,6 +352,8 @@ class Job(CommonTask): run_tasks = [] inventory_updates_actual = [] project_update_actual = None + has_setup_failures = False + setup_failure_message = "" project = self.project inventory = self.inventory @@ -359,22 +361,32 @@ class Job(CommonTask): if project.scm_update_on_launch: project_update_details = project.update_signature() if not project_update_details: - # TODO: Set error here - pass + has_setup_failures = True + setup_failure_message = "Failed to check dependent project update task" else: runnable_tasks.append({'obj': project_update_details[0], 'sig': project_update_details[1], 'type': 'project_update'}) - if is_qs.count(): + if is_qs.count() and not has_setup_failures: for inventory_source in is_qs: inventory_update_details = inventory_source.update_signature() if not inventory_update_details: - # TODO: Set error here - pass + has_setup_failures = True + setup_failure_message = "Failed to check dependent inventory update task" + break else: runnable_tasks.append({'obj': inventory_update_details[0], 'sig': inventory_update_details[1], 'type': 'inventory_update'}) + if has_setup_failures: + for each_task in runnable_tasks: + obj = each_task['obj'] + obj.status = 'error' + obj.result_traceback = setup_failure_message + obj.save() + self.status = 'error' + self.result_traceback = setup_failure_message + self.save() thisjob = {'type': 'job', 'id': self.id} for idx in xrange(len(runnable_tasks)): dependent_tasks = [{'type': r['type'], 'id': r['obj'].id} for r in runnable_tasks[idx:]] + [thisjob] From 280527f99318e50c7f46b71901594269081a68d6 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 30 Jan 2014 10:54:54 -0500 Subject: [PATCH 29/32] Enable canceling on certain levels.... fix up some unit tests --- awx/main/models/jobs.py | 1 - awx/main/tasks.py | 10 +++----- awx/main/tests/commands.py | 2 +- awx/main/tests/tasks.py | 50 +++++++++++++++++++------------------- 4 files changed, 30 insertions(+), 33 deletions(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index a2ca766ecd..e0a94d82d3 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -343,7 +343,6 @@ class Job(CommonTask): opts = dict([(field, kwargs.get(field, '')) for field in needed]) if not all(opts.values()): return False - # TODO: This is temporary to allow a dependent task to continue self.status = 'waiting' self.save(update_fields=['status']) transaction.commit() diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 482622d235..2b83597204 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -237,12 +237,10 @@ class BaseTask(Task): if logfile_pos != logfile.tell(): logfile_pos = logfile.tell() last_stdout_update = time.time() - #TODO: Find replacement for cancel flag - #TODO: Something about checking celery status - # if instance.cancel_flag: - # child.close(True) - # canceled = True - # FIXME: Find a way to determine if task is hung waiting at a prompt. + # NOTE: In case revoke doesn't have an affect + if instance.cancel_flag: + child.close(True) + canceled = True if idle_timeout and (time.time() - last_stdout_update) > idle_timeout: child.close(True) canceled = True diff --git a/awx/main/tests/commands.py b/awx/main/tests/commands.py index 05dd37c665..b25d6a04d1 100644 --- a/awx/main/tests/commands.py +++ b/awx/main/tests/commands.py @@ -377,7 +377,7 @@ class CleanupJobsTest(BaseCommandMixin, BaseLiveServerTest): self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) self.assertEqual(job.status, 'successful') # With days=1, no jobs will be deleted. diff --git a/awx/main/tests/tasks.py b/awx/main/tests/tasks.py index b781ab5fc3..63936c23d1 100644 --- a/awx/main/tests/tasks.py +++ b/awx/main/tests/tasks.py @@ -404,7 +404,7 @@ class RunJobTest(BaseCeleryTest): self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'successful') self.check_job_events(job, 'ok', 1, 2) @@ -433,7 +433,7 @@ class RunJobTest(BaseCeleryTest): self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'successful') self.check_job_events(job, 'skipped', 1, 2) @@ -461,7 +461,7 @@ class RunJobTest(BaseCeleryTest): self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'failed') self.check_job_events(job, 'failed', 1, 1) @@ -489,7 +489,7 @@ class RunJobTest(BaseCeleryTest): self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'successful') self.check_job_events(job, 'ok', 1, 1, check_ignore_errors=True) @@ -612,7 +612,7 @@ class RunJobTest(BaseCeleryTest): self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) # Since we don't actually run the task, the --check should indicate # everything is successful. @@ -653,7 +653,7 @@ class RunJobTest(BaseCeleryTest): self.assertFalse(job.passwords_needed_to_start) self.build_args_callback = self._cancel_job_callback self.assertTrue(job.start()) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'canceled') self.assertEqual(job.cancel_flag, True) @@ -676,7 +676,7 @@ class RunJobTest(BaseCeleryTest): self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'successful') self.assertTrue('--forks=3' in self.run_job_args) @@ -687,7 +687,7 @@ class RunJobTest(BaseCeleryTest): job2 = self.create_test_job(job_template=job_template2) self.assertEqual(job2.status, 'new') self.assertTrue(job2.start()) - self.assertEqual(job2.status, 'pending') + self.assertEqual(job2.status, 'waiting') job2 = Job.objects.get(pk=job2.pk) self.check_job_result(job2, 'successful') # Test with extra_vars as YAML (should be converted to JSON in args). @@ -695,7 +695,7 @@ class RunJobTest(BaseCeleryTest): job3 = self.create_test_job(job_template=job_template3) self.assertEqual(job3.status, 'new') self.assertTrue(job3.start()) - self.assertEqual(job3.status, 'pending') + self.assertEqual(job3.status, 'waiting') job3 = Job.objects.get(pk=job3.pk) self.check_job_result(job3, 'successful') @@ -707,7 +707,7 @@ class RunJobTest(BaseCeleryTest): self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) self.assertTrue(len(job.job_args) > 1024) self.check_job_result(job, 'successful') @@ -720,7 +720,7 @@ class RunJobTest(BaseCeleryTest): self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'failed') self.assertTrue('-l' in self.run_job_args) @@ -733,7 +733,7 @@ class RunJobTest(BaseCeleryTest): self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'successful') self.assertTrue('ssh-agent' in self.run_job_args) @@ -746,7 +746,7 @@ class RunJobTest(BaseCeleryTest): self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'successful') self.assertTrue('-u' in self.run_job_args) @@ -763,7 +763,7 @@ class RunJobTest(BaseCeleryTest): self.assertFalse(job.start()) self.assertEqual(job.status, 'new') self.assertTrue(job.start(ssh_password='sshpass')) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'successful') self.assertTrue('--ask-pass' in self.run_job_args) @@ -777,7 +777,7 @@ class RunJobTest(BaseCeleryTest): self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) # Job may fail if current user doesn't have password-less sudo # privileges, but we're mainly checking the command line arguments. @@ -796,7 +796,7 @@ class RunJobTest(BaseCeleryTest): self.assertFalse(job.start()) self.assertEqual(job.status, 'new') self.assertTrue(job.start(sudo_password='sudopass')) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) # Job may fail if current user doesn't have password-less sudo # privileges, but we're mainly checking the command line arguments. @@ -811,7 +811,7 @@ class RunJobTest(BaseCeleryTest): self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'successful') self.assertTrue('ssh-agent' in self.run_job_args) @@ -825,7 +825,7 @@ class RunJobTest(BaseCeleryTest): self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'successful') self.assertTrue('ssh-agent' in self.run_job_args) @@ -840,7 +840,7 @@ class RunJobTest(BaseCeleryTest): self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'failed') self.assertTrue('ssh-agent' in self.run_job_args) @@ -858,7 +858,7 @@ class RunJobTest(BaseCeleryTest): self.assertFalse(job.start()) self.assertEqual(job.status, 'new') self.assertTrue(job.start(ssh_key_unlock=TEST_SSH_KEY_DATA_UNLOCK)) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'successful') self.assertTrue('ssh-agent' in self.run_job_args) @@ -882,7 +882,7 @@ class RunJobTest(BaseCeleryTest): self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'successful') self.assertTrue(env_var1 in job.job_env) @@ -901,7 +901,7 @@ class RunJobTest(BaseCeleryTest): self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'successful') self.check_job_events(job, 'ok', 1, 1, async=True) @@ -929,7 +929,7 @@ class RunJobTest(BaseCeleryTest): self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'failed') self.check_job_events(job, 'failed', 1, 1, async=True) @@ -957,7 +957,7 @@ class RunJobTest(BaseCeleryTest): self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'failed') self.check_job_events(job, 'failed', 1, 1, async=True, @@ -986,7 +986,7 @@ class RunJobTest(BaseCeleryTest): self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'successful') self.check_job_events(job, 'ok', 1, 1, async=True, async_nowait=True) From bf8c4b289a4beabd6fb7bd28b8cfad50c9b62998 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 30 Jan 2014 11:36:54 -0500 Subject: [PATCH 30/32] Don't raise an exception at the end of a task if we are running unit tests --- awx/main/models/jobs.py | 1 - awx/main/tasks.py | 2 +- awx/main/tests/base.py | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index e0a94d82d3..6e53675363 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -391,7 +391,6 @@ class Job(CommonTask): dependent_tasks = [{'type': r['type'], 'id': r['obj'].id} for r in runnable_tasks[idx:]] + [thisjob] run_tasks.append(runnable_tasks[idx]['sig'].set(link_error=handle_work_error.s(subtasks=dependent_tasks))) run_tasks.append(task_class().si(self.pk, **opts).set(link_error=handle_work_error.s(subtasks=[thisjob]))) - print runnable_tasks res = chain(run_tasks)() return True diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 2b83597204..6f7778afe3 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -307,7 +307,7 @@ class BaseTask(Task): result_traceback=tb, output_replacements=output_replacements) self.post_run_hook(instance, **kwargs) - if status != 'successful': + if status != 'successful' and not has hasattr(settings, 'CELERY_UNIT_TEST'): # Raising an exception will mark the job as 'failed' in celery # and will stop a task chain from continuing to execute raise Exception("Task %s(pk:%s) encountered an error" % (str(self.model.__class__), str(pk))) diff --git a/awx/main/tests/base.py b/awx/main/tests/base.py index fc96dc6366..23f851d6e9 100644 --- a/awx/main/tests/base.py +++ b/awx/main/tests/base.py @@ -52,6 +52,7 @@ class BaseTestMixin(object): # callbacks. if settings.BROKER_URL.startswith('amqp://'): settings.BROKER_URL = 'django://' + settings.CELERY_UNIT_TEST = True # Make temp job status directory for unit tests. job_status_dir = tempfile.mkdtemp() self._temp_project_dirs.append(job_status_dir) From 32220405de0797cf3de9bbae0162fb01c359bd2a Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 30 Jan 2014 11:40:20 -0500 Subject: [PATCH 31/32] Jobs begin in the waiting state --- awx/main/tests/projects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/tests/projects.py b/awx/main/tests/projects.py index 199be21531..19dd4777a4 100644 --- a/awx/main/tests/projects.py +++ b/awx/main/tests/projects.py @@ -1551,7 +1551,7 @@ class ProjectUpdatesTest(BaseTransactionTest): self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) - self.assertEqual(job.status, 'pending') + self.assertEqual(job.status, 'waiting') job = Job.objects.get(pk=job.pk) self.assertTrue(job.status in ('successful', 'failed')) self.assertEqual(self.project.project_updates.count(), 3) From 28359e16a1ef497423e6b534ce2fb868c64f34f4 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 30 Jan 2014 11:41:06 -0500 Subject: [PATCH 32/32] Fix spelling mistake --- awx/main/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 6f7778afe3..99d246010d 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -307,7 +307,7 @@ class BaseTask(Task): result_traceback=tb, output_replacements=output_replacements) self.post_run_hook(instance, **kwargs) - if status != 'successful' and not has hasattr(settings, 'CELERY_UNIT_TEST'): + if status != 'successful' and not hasattr(settings, 'CELERY_UNIT_TEST'): # Raising an exception will mark the job as 'failed' in celery # and will stop a task chain from continuing to execute raise Exception("Task %s(pk:%s) encountered an error" % (str(self.model.__class__), str(pk)))
ID\n"; - html += "Status\n"; - html += "Name\n"; + html += "Status\n"; + html += "Name\n"; html += "
" + job.id + " " + job.status + "" + job.name + "" + job.id + "" + job.name + "
ID\n"; html += "Status\n"; - html += "Name\n"; + html += "Name\n"; html += "
" + job.id + "" + job.name + "" + job.name + "