From 6be2d84adbe9965818499647b3546b5e83be3a76 Mon Sep 17 00:00:00 2001 From: beeankha Date: Wed, 4 Sep 2019 12:06:37 -0400 Subject: [PATCH 01/15] Add endpoints for approval node notifications ...and also add a migration file. --- awx/api/serializers.py | 8 ++- awx/api/urls/organization.py | 3 + awx/api/urls/workflow_approval_template.py | 3 + awx/api/urls/workflow_job_template.py | 3 + awx/api/views/__init__.py | 18 ++++++ awx/api/views/organization.py | 5 ++ .../0088_v360_approval_node_notifications.py | 28 +++++++++ awx/main/models/base.py | 7 +++ awx/main/models/notifications.py | 5 +- awx/main/models/unified_jobs.py | 4 ++ awx/main/models/workflow.py | 58 ++++++++++++++++++- awx/main/scheduler/task_manager.py | 6 ++ 12 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 awx/main/migrations/0088_v360_approval_node_notifications.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index c7a322489b..84e1ed4051 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1261,6 +1261,7 @@ class OrganizationSerializer(BaseSerializer): notification_templates_started = self.reverse('api:organization_notification_templates_started_list', kwargs={'pk': obj.pk}), notification_templates_success = self.reverse('api:organization_notification_templates_success_list', kwargs={'pk': obj.pk}), notification_templates_error = self.reverse('api:organization_notification_templates_error_list', kwargs={'pk': obj.pk}), + notification_templates_approvals = self.reverse('api:organization_notification_templates_approvals_list', kwargs={'pk': obj.pk}), object_roles = self.reverse('api:organization_object_roles_list', kwargs={'pk': obj.pk}), access_list = self.reverse('api:organization_access_list', kwargs={'pk': obj.pk}), instance_groups = self.reverse('api:organization_instance_groups_list', kwargs={'pk': obj.pk}), @@ -3335,6 +3336,7 @@ class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJo notification_templates_started = self.reverse('api:workflow_job_template_notification_templates_started_list', kwargs={'pk': obj.pk}), notification_templates_success = self.reverse('api:workflow_job_template_notification_templates_success_list', kwargs={'pk': obj.pk}), notification_templates_error = self.reverse('api:workflow_job_template_notification_templates_error_list', kwargs={'pk': obj.pk}), + notification_templates_approvals = self.reverse('api:workflow_job_template_notification_templates_approvals_list', kwargs={'pk': obj.pk}), access_list = self.reverse('api:workflow_job_template_access_list', kwargs={'pk': obj.pk}), object_roles = self.reverse('api:workflow_job_template_object_roles_list', kwargs={'pk': obj.pk}), survey_spec = self.reverse('api:workflow_job_template_survey_spec', kwargs={'pk': obj.pk}), @@ -3490,7 +3492,11 @@ class WorkflowApprovalTemplateSerializer(UnifiedJobTemplateSerializer): if 'last_job' in res: del res['last_job'] - res.update(dict(jobs = self.reverse('api:workflow_approval_template_jobs_list', kwargs={'pk': obj.pk}),)) + # Placeholder... + res.update(dict( + jobs = self.reverse('api:workflow_approval_template_jobs_list', kwargs={'pk': obj.pk}), + approval_notifications = self.reverse('api:workflow_approval_template_notification_list', kwargs={'pk': obj.pk}), + )) return res diff --git a/awx/api/urls/organization.py b/awx/api/urls/organization.py index 952209423e..1b0997b05c 100644 --- a/awx/api/urls/organization.py +++ b/awx/api/urls/organization.py @@ -18,6 +18,7 @@ from awx.api.views import ( OrganizationNotificationTemplatesErrorList, OrganizationNotificationTemplatesStartedList, OrganizationNotificationTemplatesSuccessList, + OrganizationNotificationTemplatesApprovalList, OrganizationInstanceGroupsList, OrganizationObjectRolesList, OrganizationAccessList, @@ -43,6 +44,8 @@ urls = [ name='organization_notification_templates_error_list'), url(r'^(?P[0-9]+)/notification_templates_success/$', OrganizationNotificationTemplatesSuccessList.as_view(), name='organization_notification_templates_success_list'), + url(r'^(?P[0-9]+)/notification_templates_approvals/$', OrganizationNotificationTemplatesApprovalList.as_view(), + name='organization_notification_templates_approvals_list'), url(r'^(?P[0-9]+)/instance_groups/$', OrganizationInstanceGroupsList.as_view(), name='organization_instance_groups_list'), url(r'^(?P[0-9]+)/object_roles/$', OrganizationObjectRolesList.as_view(), name='organization_object_roles_list'), url(r'^(?P[0-9]+)/access_list/$', OrganizationAccessList.as_view(), name='organization_access_list'), diff --git a/awx/api/urls/workflow_approval_template.py b/awx/api/urls/workflow_approval_template.py index 8a22ee83b3..af25a0d1e1 100644 --- a/awx/api/urls/workflow_approval_template.py +++ b/awx/api/urls/workflow_approval_template.py @@ -6,12 +6,15 @@ from django.conf.urls import url from awx.api.views import ( WorkflowApprovalTemplateDetail, WorkflowApprovalTemplateJobsList, + WorkflowApprovalNotificationTemplatesList, ) urls = [ url(r'^(?P[0-9]+)/$', WorkflowApprovalTemplateDetail.as_view(), name='workflow_approval_template_detail'), url(r'^(?P[0-9]+)/approvals/$', WorkflowApprovalTemplateJobsList.as_view(), name='workflow_approval_template_jobs_list'), + url(r'^(?P[0-9]+)/approval_notifications/$', WorkflowApprovalNotificationTemplatesList.as_view(), + name='workflow_approval_template_notification_list'), ] __all__ = ['urls'] diff --git a/awx/api/urls/workflow_job_template.py b/awx/api/urls/workflow_job_template.py index 0a33a8eaaa..349dad1aa5 100644 --- a/awx/api/urls/workflow_job_template.py +++ b/awx/api/urls/workflow_job_template.py @@ -16,6 +16,7 @@ from awx.api.views import ( WorkflowJobTemplateNotificationTemplatesErrorList, WorkflowJobTemplateNotificationTemplatesStartedList, WorkflowJobTemplateNotificationTemplatesSuccessList, + WorkflowJobTemplateNotificationTemplatesApprovalList, WorkflowJobTemplateAccessList, WorkflowJobTemplateObjectRolesList, WorkflowJobTemplateLabelList, @@ -38,6 +39,8 @@ urls = [ name='workflow_job_template_notification_templates_error_list'), url(r'^(?P[0-9]+)/notification_templates_success/$', WorkflowJobTemplateNotificationTemplatesSuccessList.as_view(), name='workflow_job_template_notification_templates_success_list'), + url(r'^(?P[0-9]+)/notification_templates_approvals/$', WorkflowJobTemplateNotificationTemplatesApprovalList.as_view(), + name='workflow_job_template_notification_templates_approvals_list'), url(r'^(?P[0-9]+)/access_list/$', WorkflowJobTemplateAccessList.as_view(), name='workflow_job_template_access_list'), url(r'^(?P[0-9]+)/object_roles/$', WorkflowJobTemplateObjectRolesList.as_view(), name='workflow_job_template_object_roles_list'), url(r'^(?P[0-9]+)/labels/$', WorkflowJobTemplateLabelList.as_view(), name='workflow_job_template_label_list'), diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index d77ec92b91..8719ac7be4 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -119,6 +119,7 @@ from awx.api.views.organization import ( # noqa OrganizationNotificationTemplatesErrorList, OrganizationNotificationTemplatesStartedList, OrganizationNotificationTemplatesSuccessList, + OrganizationNotificationTemplatesApprovalList, OrganizationInstanceGroupsList, OrganizationAccessList, OrganizationObjectRolesList, @@ -3288,6 +3289,11 @@ class WorkflowJobTemplateNotificationTemplatesSuccessList(WorkflowJobTemplateNot relationship = 'notification_templates_success' +class WorkflowJobTemplateNotificationTemplatesApprovalList(WorkflowJobTemplateNotificationTemplatesAnyList): + + relationship = 'approval_notifications' + + class WorkflowJobTemplateAccessList(ResourceAccessList): model = models.User # needs to be User for AccessLists's @@ -4458,6 +4464,18 @@ class WorkflowApprovalTemplateJobsList(SubListAPIView): parent_key = 'workflow_approval_template' +class WorkflowApprovalTemplateNotificationTemplatesList(SubListCreateAttachDetachAPIView): + + model = models.NotificationTemplate + serializer_class = serializers.NotificationTemplateSerializer + parent_model = models.WorkflowApprovalTemplate + + +class WorkflowApprovalNotificationTemplatesList(WorkflowApprovalTemplateNotificationTemplatesList): + + relationship = 'approval_notifications' + + class WorkflowApprovalList(ListCreateAPIView): model = models.WorkflowApproval diff --git a/awx/api/views/organization.py b/awx/api/views/organization.py index 6213e14f63..2f3fed49ac 100644 --- a/awx/api/views/organization.py +++ b/awx/api/views/organization.py @@ -195,6 +195,11 @@ class OrganizationNotificationTemplatesSuccessList(OrganizationNotificationTempl relationship = 'notification_templates_success' +class OrganizationNotificationTemplatesApprovalList(OrganizationNotificationTemplatesAnyList): + + relationship = 'approval_notifications' + + class OrganizationInstanceGroupsList(SubListAttachDetachAPIView): model = InstanceGroup diff --git a/awx/main/migrations/0088_v360_approval_node_notifications.py b/awx/main/migrations/0088_v360_approval_node_notifications.py new file mode 100644 index 0000000000..bb11b93b0d --- /dev/null +++ b/awx/main/migrations/0088_v360_approval_node_notifications.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.4 on 2019-09-03 19:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0087_v360_update_credential_injector_help_text'), + ] + + operations = [ + migrations.AddField( + model_name='organization', + name='approval_notifications', + field=models.ManyToManyField(blank=True, related_name='organization_approval_notifications', to='main.NotificationTemplate'), + ), + migrations.AddField( + model_name='unifiedjobtemplate', + name='approval_notifications', + field=models.ManyToManyField(blank=True, related_name='unifiedjobtemplate_approval_notifications', to='main.NotificationTemplate'), + ), + migrations.AlterField( + model_name='workflowjobnode', + name='do_not_run', + field=models.BooleanField(default=False, help_text='Indicates that a job will not be created when True. Workflow runtime semantics will mark this True if the node is in a path that will decidedly not be ran. A value of False means the node may not run.'), + ), + ] diff --git a/awx/main/models/base.py b/awx/main/models/base.py index 70fa92cf0a..97aa94f1d8 100644 --- a/awx/main/models/base.py +++ b/awx/main/models/base.py @@ -392,6 +392,13 @@ class NotificationFieldsModel(BaseModel): related_name='%(class)s_notification_templates_for_started' ) + # Placeholder, unsure if this is required... + notification_templates_approvals = models.ManyToManyField( + "NotificationTemplate", + blank=True, + related_name='%(class)s_notification_templates_for_approvals' + ) + def prevent_search(relation): """ diff --git a/awx/main/models/notifications.py b/awx/main/models/notifications.py index ef428bcdfa..588e97db3b 100644 --- a/awx/main/models/notifications.py +++ b/awx/main/models/notifications.py @@ -463,8 +463,9 @@ class JobNotificationMixin(object): def send_notification_templates(self, status): from awx.main.tasks import send_notifications # avoid circular import - if status not in ['running', 'succeeded', 'failed']: - raise ValueError(_("status must be either running, succeeded or failed")) + # Placeholder... Adding "pending" status here for approvals. + if status not in ['pending', 'running', 'succeeded', 'failed']: + raise ValueError(_("status must be either pending, running, succeeded or failed")) try: notification_templates = self.get_notification_templates() except Exception: diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index b8dd1d1cf8..d4fe22806c 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -1217,6 +1217,10 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique status=self.status, traceback=self.result_traceback) + # Placeholder... + # def approval_notification_data(self): + # for approval in WorkflowApproval.objects.filter(workflow_approval_template=instance, status='pending'): + def pre_start(self, **kwargs): if not self.can_start: self.job_explanation = u'%s is not in a startable state: %s, expecting one of %s' % (self._meta.verbose_name, self.status, str(('new', 'waiting'))) diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index b2312ab63d..6d8bf14019 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -196,7 +196,7 @@ class WorkflowJobNode(WorkflowNodeBase): ) do_not_run = models.BooleanField( default=False, - help_text=_("Indidcates that a job will not be created when True. Workflow runtime " + help_text=_("Indicates that a job will not be created when True. Workflow runtime " "semantics will mark this True if the node is in a path that will " "decidedly not be ran. A value of False means the node may not run."), ) @@ -441,9 +441,12 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl .filter(unifiedjobtemplate_notification_templates_for_started__in=[self])) success_notification_templates = list(base_notification_templates .filter(unifiedjobtemplate_notification_templates_for_success__in=[self])) + approval_notification_templates = list(base_notification_templates + .filter(unifiedjobtemplate_notification_templates_for_approvals__in=[self])) return dict(error=list(error_notification_templates), started=list(started_notification_templates), - success=list(success_notification_templates)) + success=list(success_notification_templates), + approvals=list(approval_notification_templates)) def create_unified_job(self, **kwargs): workflow_job = super(WorkflowJobTemplate, self).create_unified_job(**kwargs) @@ -644,12 +647,46 @@ class WorkflowApprovalTemplate(UnifiedJobTemplate): def get_absolute_url(self, request=None): return reverse('api:workflow_approval_template_detail', kwargs={'pk': self.pk}, request=request) + @property + def notification_templates(self): + base_notification_templates = NotificationTemplate.objects.all() + error_notification_templates = list(base_notification_templates + .filter(unifiedjobtemplate_notification_templates_for_errors__in=[self])) + started_notification_templates = list(base_notification_templates + .filter(unifiedjobtemplate_notification_templates_for_started__in=[self])) + success_notification_templates = list(base_notification_templates + .filter(unifiedjobtemplate_notification_templates_for_success__in=[self])) + return dict(error=list(error_notification_templates), + started=list(started_notification_templates), + success=list(success_notification_templates)) + + # base_notification_templates = NotificationTemplate.objects.all() + # approval_notification_templates = list(base_notification_templates + # .filter(unifiedjobtemplate_notification_templates_for_errors__in=[self]), + # base_notification_templates + # .filter(unifiedjobtemplate_notification_templates_for_started__in=[self]), + # base_notification_templates + # .filter(unifiedjobtemplate_notification_templates_for_success__in=[self])) + # return dict(approval=list(approval_notification_templates)) +# Placeholder... Approval nodes don't have orgs! + # if self.project is not None and self.project.organization is None: + # error_notification_templates = set(error_notification_templates + list(base_notification_templates.filter( + # organization_notification_templates_for_errors=self.project.organization))) + # started_notification_templates = set(started_notification_templates + list(base_notification_templates.filter( + # organization_notification_templates_for_started=self.project.organization))) + # success_notification_templates = set(success_notification_templates + list(base_notification_templates.filter( + # organization_notification_templates_for_success=self.project.organization))) + # return dict(error=list(error_notification_templates), + # approval_notification_templates=list(needs_approval_notification_templates), + # success=list(success_notification_templates)) + + @property def workflow_job_template(self): return self.workflowjobtemplatenodes.first().workflow_job_template -class WorkflowApproval(UnifiedJob): +class WorkflowApproval(UnifiedJob, JobNotificationMixin): class Meta: app_label = 'main' @@ -700,6 +737,21 @@ class WorkflowApproval(UnifiedJob): schedule_task_manager() return reverse('api:workflow_approval_deny', kwargs={'pk': self.pk}, request=request) + # Placeholder... + def approval_notification_data(self): + result = super(WorkflowApproval, self).approval_notification_data() + str_arr = ['Approval summary:', ''] + for node in self.workflow_job_nodes.all().select_related('job'): + if node.job is None: + node_job_description = 'no job.' + else: + node_job_description = ('job #{0}, "{1}", which finished with status {2}.' + .format(node.job.id, node.job.name, node.job.status)) + str_arr.append("- node #{0} spawns {1}".format(node.id, node_job_description)) + result['body'] = '\n'.join(str_arr) + return result + + @property def workflow_job_template(self): try: diff --git a/awx/main/scheduler/task_manager.py b/awx/main/scheduler/task_manager.py index 3d89d6aecc..fbd2edd1ea 100644 --- a/awx/main/scheduler/task_manager.py +++ b/awx/main/scheduler/task_manager.py @@ -239,6 +239,12 @@ class TaskManager(): task.send_notification_templates('running') logger.debug('Transitioning %s to running status.', task.log_format) schedule_task_manager() + # Placeholder... + elif type(task) is WorkflowApproval: + task.status = 'pending' + task.send_notification_templates('pending') + logger.debug('Transitioning %s to pending status.', task.log_format) + schedule_task_manager() elif not task.supports_isolation() and rampart_group.controller_id: # non-Ansible jobs on isolated instances run on controller task.instance_group = rampart_group.controller From 13450fdbf950d239b85025dd88d98d34344588a3 Mon Sep 17 00:00:00 2001 From: beeankha Date: Wed, 4 Sep 2019 18:04:43 -0400 Subject: [PATCH 02/15] Set up approval notifications to send --- awx/api/serializers.py | 2 +- awx/api/views/__init__.py | 4 +- awx/api/views/organization.py | 2 +- .../0088_v360_approval_node_notifications.py | 10 +-- awx/main/models/notifications.py | 1 - awx/main/models/workflow.py | 73 +++++++------------ awx/main/scheduler/task_manager.py | 7 +- 7 files changed, 35 insertions(+), 64 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 84e1ed4051..c09c9a1ddc 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -3495,7 +3495,7 @@ class WorkflowApprovalTemplateSerializer(UnifiedJobTemplateSerializer): # Placeholder... res.update(dict( jobs = self.reverse('api:workflow_approval_template_jobs_list', kwargs={'pk': obj.pk}), - approval_notifications = self.reverse('api:workflow_approval_template_notification_list', kwargs={'pk': obj.pk}), + approval_notification_templates = self.reverse('api:workflow_approval_template_notification_list', kwargs={'pk': obj.pk}), )) return res diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 8719ac7be4..92674b1d7d 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -3291,7 +3291,7 @@ class WorkflowJobTemplateNotificationTemplatesSuccessList(WorkflowJobTemplateNot class WorkflowJobTemplateNotificationTemplatesApprovalList(WorkflowJobTemplateNotificationTemplatesAnyList): - relationship = 'approval_notifications' + relationship = 'notification_templates_approvals' class WorkflowJobTemplateAccessList(ResourceAccessList): @@ -4473,7 +4473,7 @@ class WorkflowApprovalTemplateNotificationTemplatesList(SubListCreateAttachDetac class WorkflowApprovalNotificationTemplatesList(WorkflowApprovalTemplateNotificationTemplatesList): - relationship = 'approval_notifications' + relationship = 'notification_templates_approvals' class WorkflowApprovalList(ListCreateAPIView): diff --git a/awx/api/views/organization.py b/awx/api/views/organization.py index 2f3fed49ac..e1af4c67b1 100644 --- a/awx/api/views/organization.py +++ b/awx/api/views/organization.py @@ -197,7 +197,7 @@ class OrganizationNotificationTemplatesSuccessList(OrganizationNotificationTempl class OrganizationNotificationTemplatesApprovalList(OrganizationNotificationTemplatesAnyList): - relationship = 'approval_notifications' + relationship = 'notification_templates_approvals' class OrganizationInstanceGroupsList(SubListAttachDetachAPIView): diff --git a/awx/main/migrations/0088_v360_approval_node_notifications.py b/awx/main/migrations/0088_v360_approval_node_notifications.py index bb11b93b0d..d24051c18e 100644 --- a/awx/main/migrations/0088_v360_approval_node_notifications.py +++ b/awx/main/migrations/0088_v360_approval_node_notifications.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.4 on 2019-09-03 19:25 +# Generated by Django 2.2.4 on 2019-09-04 18:23 from django.db import migrations, models @@ -12,13 +12,13 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( model_name='organization', - name='approval_notifications', - field=models.ManyToManyField(blank=True, related_name='organization_approval_notifications', to='main.NotificationTemplate'), + name='notification_templates_approvals', + field=models.ManyToManyField(blank=True, related_name='organization_notification_templates_for_approvals', to='main.NotificationTemplate'), ), migrations.AddField( model_name='unifiedjobtemplate', - name='approval_notifications', - field=models.ManyToManyField(blank=True, related_name='unifiedjobtemplate_approval_notifications', to='main.NotificationTemplate'), + name='notification_templates_approvals', + field=models.ManyToManyField(blank=True, related_name='unifiedjobtemplate_notification_templates_for_approvals', to='main.NotificationTemplate'), ), migrations.AlterField( model_name='workflowjobnode', diff --git a/awx/main/models/notifications.py b/awx/main/models/notifications.py index 588e97db3b..2d2a692824 100644 --- a/awx/main/models/notifications.py +++ b/awx/main/models/notifications.py @@ -463,7 +463,6 @@ class JobNotificationMixin(object): def send_notification_templates(self, status): from awx.main.tasks import send_notifications # avoid circular import - # Placeholder... Adding "pending" status here for approvals. if status not in ['pending', 'running', 'succeeded', 'failed']: raise ValueError(_("status must be either pending, running, succeeded or failed")) try: diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index 6d8bf14019..d92a21b394 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -5,7 +5,7 @@ import logging # Django -from django.db import models +from django.db import connection, models from django.conf import settings from django.utils.translation import ugettext_lazy as _ from django.core.exceptions import ObjectDoesNotExist @@ -577,6 +577,13 @@ class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificatio for node in self.workflow_job_nodes.all().select_related('job'): if node.job is None: node_job_description = 'no job.' + elif type(node.unified_job_template) is WorkflowApprovalTemplate: + if node.job.status == 'pending': + node_job_description = 'APPROVE THIS!!!' + if node.job.status == 'successful': + node_job_description = 'THIS GOT APPROVED!!!' + if node.job.status == 'failed': + node_job_description = 'DENIED!!!' else: node_job_description = ('job #{0}, "{1}", which finished with status {2}.' .format(node.job.id, node.job.name, node.job.status)) @@ -647,40 +654,6 @@ class WorkflowApprovalTemplate(UnifiedJobTemplate): def get_absolute_url(self, request=None): return reverse('api:workflow_approval_template_detail', kwargs={'pk': self.pk}, request=request) - @property - def notification_templates(self): - base_notification_templates = NotificationTemplate.objects.all() - error_notification_templates = list(base_notification_templates - .filter(unifiedjobtemplate_notification_templates_for_errors__in=[self])) - started_notification_templates = list(base_notification_templates - .filter(unifiedjobtemplate_notification_templates_for_started__in=[self])) - success_notification_templates = list(base_notification_templates - .filter(unifiedjobtemplate_notification_templates_for_success__in=[self])) - return dict(error=list(error_notification_templates), - started=list(started_notification_templates), - success=list(success_notification_templates)) - - # base_notification_templates = NotificationTemplate.objects.all() - # approval_notification_templates = list(base_notification_templates - # .filter(unifiedjobtemplate_notification_templates_for_errors__in=[self]), - # base_notification_templates - # .filter(unifiedjobtemplate_notification_templates_for_started__in=[self]), - # base_notification_templates - # .filter(unifiedjobtemplate_notification_templates_for_success__in=[self])) - # return dict(approval=list(approval_notification_templates)) -# Placeholder... Approval nodes don't have orgs! - # if self.project is not None and self.project.organization is None: - # error_notification_templates = set(error_notification_templates + list(base_notification_templates.filter( - # organization_notification_templates_for_errors=self.project.organization))) - # started_notification_templates = set(started_notification_templates + list(base_notification_templates.filter( - # organization_notification_templates_for_started=self.project.organization))) - # success_notification_templates = set(success_notification_templates + list(base_notification_templates.filter( - # organization_notification_templates_for_success=self.project.organization))) - # return dict(error=list(error_notification_templates), - # approval_notification_templates=list(needs_approval_notification_templates), - # success=list(success_notification_templates)) - - @property def workflow_job_template(self): return self.workflowjobtemplatenodes.first().workflow_job_template @@ -726,6 +699,7 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin): def approve(self, request=None): self.status = 'successful' self.save() + self.send_approval_notification(self.status) self.websocket_emit_status(self.status) schedule_task_manager() return reverse('api:workflow_approval_approve', kwargs={'pk': self.pk}, request=request) @@ -733,24 +707,27 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin): def deny(self, request=None): self.status = 'failed' self.save() + self.send_approval_notification(self.status) self.websocket_emit_status(self.status) schedule_task_manager() return reverse('api:workflow_approval_deny', kwargs={'pk': self.pk}, request=request) - # Placeholder... - def approval_notification_data(self): - result = super(WorkflowApproval, self).approval_notification_data() - str_arr = ['Approval summary:', ''] - for node in self.workflow_job_nodes.all().select_related('job'): - if node.job is None: - node_job_description = 'no job.' - else: - node_job_description = ('job #{0}, "{1}", which finished with status {2}.' - .format(node.job.id, node.job.name, node.job.status)) - str_arr.append("- node #{0} spawns {1}".format(node.id, node_job_description)) - result['body'] = '\n'.join(str_arr) - return result + def signal_start(self, **kwargs): + can_start = super(WorkflowApproval, self).signal_start(**kwargs) + self.send_approval_notification('running') + return can_start + def send_approval_notification(self, status): + from awx.main.tasks import send_notifications # avoid circular import + for nt in self.workflow_job_template.notification_templates["approvals"]: + (notification_subject, notification_body) = self.workflow_job.build_notification_message(nt, status) + + def send_it(local_nt=nt, local_subject=notification_subject, local_body=notification_body): + def _func(): + send_notifications.delay([local_nt.generate_notification(local_subject, local_body).id], + job_id=self.id) + return _func + connection.on_commit(send_it()) @property def workflow_job_template(self): diff --git a/awx/main/scheduler/task_manager.py b/awx/main/scheduler/task_manager.py index fbd2edd1ea..609ba163e0 100644 --- a/awx/main/scheduler/task_manager.py +++ b/awx/main/scheduler/task_manager.py @@ -239,12 +239,6 @@ class TaskManager(): task.send_notification_templates('running') logger.debug('Transitioning %s to running status.', task.log_format) schedule_task_manager() - # Placeholder... - elif type(task) is WorkflowApproval: - task.status = 'pending' - task.send_notification_templates('pending') - logger.debug('Transitioning %s to pending status.', task.log_format) - schedule_task_manager() elif not task.supports_isolation() and rampart_group.controller_id: # non-Ansible jobs on isolated instances run on controller task.instance_group = rampart_group.controller @@ -539,6 +533,7 @@ class TaskManager(): logger.warn(timeout_message) task.timed_out = True task.status = 'failed' + self.send_approval_notification(task.status) task.websocket_emit_status(task.status) task.job_explanation = timeout_message task.save(update_fields=['status', 'job_explanation', 'timed_out']) From e2b8adcd09aad7a8dd5669b82b77358465d25099 Mon Sep 17 00:00:00 2001 From: beeankha Date: Thu, 5 Sep 2019 12:02:27 -0400 Subject: [PATCH 03/15] Remove notification endpoint from approvals list --- awx/api/serializers.py | 2 -- awx/api/urls/workflow_approval_template.py | 3 --- awx/api/views/__init__.py | 12 ------------ 3 files changed, 17 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index c09c9a1ddc..11d31e8856 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -3492,10 +3492,8 @@ class WorkflowApprovalTemplateSerializer(UnifiedJobTemplateSerializer): if 'last_job' in res: del res['last_job'] - # Placeholder... res.update(dict( jobs = self.reverse('api:workflow_approval_template_jobs_list', kwargs={'pk': obj.pk}), - approval_notification_templates = self.reverse('api:workflow_approval_template_notification_list', kwargs={'pk': obj.pk}), )) return res diff --git a/awx/api/urls/workflow_approval_template.py b/awx/api/urls/workflow_approval_template.py index af25a0d1e1..8a22ee83b3 100644 --- a/awx/api/urls/workflow_approval_template.py +++ b/awx/api/urls/workflow_approval_template.py @@ -6,15 +6,12 @@ from django.conf.urls import url from awx.api.views import ( WorkflowApprovalTemplateDetail, WorkflowApprovalTemplateJobsList, - WorkflowApprovalNotificationTemplatesList, ) urls = [ url(r'^(?P[0-9]+)/$', WorkflowApprovalTemplateDetail.as_view(), name='workflow_approval_template_detail'), url(r'^(?P[0-9]+)/approvals/$', WorkflowApprovalTemplateJobsList.as_view(), name='workflow_approval_template_jobs_list'), - url(r'^(?P[0-9]+)/approval_notifications/$', WorkflowApprovalNotificationTemplatesList.as_view(), - name='workflow_approval_template_notification_list'), ] __all__ = ['urls'] diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 92674b1d7d..90253c448f 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -4464,18 +4464,6 @@ class WorkflowApprovalTemplateJobsList(SubListAPIView): parent_key = 'workflow_approval_template' -class WorkflowApprovalTemplateNotificationTemplatesList(SubListCreateAttachDetachAPIView): - - model = models.NotificationTemplate - serializer_class = serializers.NotificationTemplateSerializer - parent_model = models.WorkflowApprovalTemplate - - -class WorkflowApprovalNotificationTemplatesList(WorkflowApprovalTemplateNotificationTemplatesList): - - relationship = 'notification_templates_approvals' - - class WorkflowApprovalList(ListCreateAPIView): model = models.WorkflowApproval From 7eb7aad4917b24d40a4476060cd11eb4809510b6 Mon Sep 17 00:00:00 2001 From: mabashian Date: Thu, 5 Sep 2019 14:44:59 -0400 Subject: [PATCH 04/15] Adds approval toggles to wf and org notif lists --- .../src/notifications/notifications.list.js | 13 ++++++++++++ .../shared/notification-list-init.factory.js | 20 +++++++++++++++++-- awx/ui/client/src/shared/generator-helpers.js | 3 ++- .../list-generator/list-generator.factory.js | 2 ++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/src/notifications/notifications.list.js b/awx/ui/client/src/notifications/notifications.list.js index d146465f87..a08dd7692e 100644 --- a/awx/ui/client/src/notifications/notifications.list.js +++ b/awx/ui/client/src/notifications/notifications.list.js @@ -35,6 +35,19 @@ export default ['i18n', 'templateUrl', function(i18n, templateUrl){ excludeModal: true, columnClass: 'd-none d-sm-flex col-md-4 col-sm-3' }, + notification_templates_approvals: { + label: i18n._('Approval'), + columnClass: 'd-none d-md-flex justify-content-start col-md-1', + flag: 'notification_templates_approvals', + type: "toggle", + ngClick: "toggleNotification($event, notification.id, 'notification_templates_approvals')", + ngDisabled: "!sufficientRoleForNotifToggle", + awToolTip: "{{ schedule.play_tip }}", + dataTipWatch: "schedule.play_tip", + dataPlacement: "right", + nosort: true, + ngIf: "showApprovalColumn" + }, notification_templates_started: { label: i18n._("Start"), flag: 'notification_templates_started', diff --git a/awx/ui/client/src/notifications/shared/notification-list-init.factory.js b/awx/ui/client/src/notifications/shared/notification-list-init.factory.js index 03027feab9..7829f9e781 100644 --- a/awx/ui/client/src/notifications/shared/notification-list-init.factory.js +++ b/awx/ui/client/src/notifications/shared/notification-list-init.factory.js @@ -22,6 +22,10 @@ export default ['Wait', 'GetBasePath', 'ProcessErrors', 'Rest', 'GetChoices', url = params.url, id = params.id; + if ($state.includes('templates.editWorkflowJobTemplate') || $state.includes('organizations.edit')) { + scope.showApprovalColumn = true; + } + scope.addNotificationTemplate = function() { var org_id; if($stateParams.hasOwnProperty('project_id')){ @@ -51,6 +55,10 @@ export default ['Wait', 'GetBasePath', 'ProcessErrors', 'Rest', 'GetChoices', scope.relatednotificationsRemove = scope.$on('relatednotifications', function () { var columns = ['/notification_templates_started/', '/notification_templates_success/', '/notification_templates_error/']; + if ($state.includes('templates.editWorkflowJobTemplate') || $state.includes('organizations.edit')) { + columns.push('/notification_templates_approvals'); + } + GetChoices({ scope: scope, url: GetBasePath('notifications'), @@ -64,9 +72,17 @@ export default ['Wait', 'GetBasePath', 'ProcessErrors', 'Rest', 'GetChoices', Rest.setUrl(notifier_url); Rest.get() .then(function(response) { - let checkForSuccessOrError = response.config.url.indexOf('success') > 0 ? "notification_templates_success" : "notification_templates_error"; + let type; - let type = response.config.url.indexOf('started') > 0 ? "notification_templates_started" : checkForSuccessOrError; + if (response.config.url.indexOf('started') > 0) { + type = "notification_templates_started"; + } else if (response.config.url.indexOf('success') > 0) { + type = "notification_templates_success"; + } else if (response.config.url.indexOf('error') > 0) { + type = "notification_templates_error"; + } else if (response.config.url.indexOf('approvals') > 0) { + type = "notification_templates_approvals"; + } if (response.data.results) { _.forEach(response.data.results, function(result){ diff --git a/awx/ui/client/src/shared/generator-helpers.js b/awx/ui/client/src/shared/generator-helpers.js index 60af4a17aa..c0489bd941 100644 --- a/awx/ui/client/src/shared/generator-helpers.js +++ b/awx/ui/client/src/shared/generator-helpers.js @@ -527,8 +527,9 @@ angular.module('GeneratorHelpers', [systemStatus.name]) } else if (field.type === 'template') { html = Template(field); } else if (field.type === 'toggle') { + const ngIf = field.ngIf ? `ng-if="${field.ngIf}"` : ''; html += ` -
+
`; diff --git a/awx/ui/client/src/shared/list-generator/list-generator.factory.js b/awx/ui/client/src/shared/list-generator/list-generator.factory.js index 11e932cc09..b63fa96f9b 100644 --- a/awx/ui/client/src/shared/list-generator/list-generator.factory.js +++ b/awx/ui/client/src/shared/list-generator/list-generator.factory.js @@ -546,7 +546,9 @@ export default ['$compile', 'Attr', 'Icon', for (fld in list.fields) { if (options.mode !== 'lookup' || (options.mode === 'lookup' && (fld === 'name' || _.has(list.fields[fld], 'includeModal')))){ let customClass = list.fields[fld].columnClass || ''; + const ngIf = list.fields[fld].ngIf ? `ng-if="${list.fields[fld].ngIf}"` : ''; html += `
Date: Fri, 6 Sep 2019 22:07:13 -0400 Subject: [PATCH 05/15] Set default messages for approval notifications --- awx/main/models/workflow.py | 36 +++++++++++++++++++++--------- awx/main/scheduler/task_manager.py | 2 +- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index d92a21b394..495b553877 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -577,13 +577,6 @@ class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificatio for node in self.workflow_job_nodes.all().select_related('job'): if node.job is None: node_job_description = 'no job.' - elif type(node.unified_job_template) is WorkflowApprovalTemplate: - if node.job.status == 'pending': - node_job_description = 'APPROVE THIS!!!' - if node.job.status == 'successful': - node_job_description = 'THIS GOT APPROVED!!!' - if node.job.status == 'failed': - node_job_description = 'DENIED!!!' else: node_job_description = ('job #{0}, "{1}", which finished with status {2}.' .format(node.job.id, node.job.name, node.job.status)) @@ -699,7 +692,7 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin): def approve(self, request=None): self.status = 'successful' self.save() - self.send_approval_notification(self.status) + self.send_approval_notification('approved') self.websocket_emit_status(self.status) schedule_task_manager() return reverse('api:workflow_approval_approve', kwargs={'pk': self.pk}, request=request) @@ -707,7 +700,7 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin): def deny(self, request=None): self.status = 'failed' self.save() - self.send_approval_notification(self.status) + self.send_approval_notification('denied') self.websocket_emit_status(self.status) schedule_task_manager() return reverse('api:workflow_approval_deny', kwargs={'pk': self.pk}, request=request) @@ -720,7 +713,10 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin): def send_approval_notification(self, status): from awx.main.tasks import send_notifications # avoid circular import for nt in self.workflow_job_template.notification_templates["approvals"]: - (notification_subject, notification_body) = self.workflow_job.build_notification_message(nt, status) + try: + (notification_subject, notification_body) = self.build_notification_message(nt, status) + except AttributeError: + raise NotImplementedError("build_notification_message() does not exist" % status) def send_it(local_nt=nt, local_subject=notification_subject, local_body=notification_body): def _func(): @@ -729,6 +725,26 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin): return _func connection.on_commit(send_it()) + def build_notification_message(self, nt, status): + subject = [] + subject.append(('{}, Job Number {}').format(self.workflow_approval_template.name, + self.id)) + if status == 'running': + subject.append('Please approve or deny this node.') + if status == 'approved': + subject.append('This approval node was approved.') + if status == 'timed_out': + subject.append('This approval node has timed out.') + elif status == 'denied': + subject.append('This approval node was denied.') + body = self.notification_data() + body['body'] = subject + + return subject, body + + # def get_notification_templates(self): + # return self.workflow_job_template.notification_templates + @property def workflow_job_template(self): try: diff --git a/awx/main/scheduler/task_manager.py b/awx/main/scheduler/task_manager.py index 609ba163e0..73218b6f81 100644 --- a/awx/main/scheduler/task_manager.py +++ b/awx/main/scheduler/task_manager.py @@ -533,7 +533,7 @@ class TaskManager(): logger.warn(timeout_message) task.timed_out = True task.status = 'failed' - self.send_approval_notification(task.status) + task.send_approval_notification('timed_out') task.websocket_emit_status(task.status) task.job_explanation = timeout_message task.save(update_fields=['status', 'job_explanation', 'timed_out']) From aa5a4d42c77c34dbf05ee448053519ebaa028ada Mon Sep 17 00:00:00 2001 From: beeankha Date: Tue, 10 Sep 2019 11:04:32 -0400 Subject: [PATCH 06/15] Enable email notifications to work, ...and customize default messages --- awx/main/models/workflow.py | 23 +++++++++++------------ awx/main/tasks.py | 2 +- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index 495b553877..b736dc503f 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -3,6 +3,8 @@ # Python import logging +from copy import copy +from urllib.parse import urljoin # Django from django.db import connection, models @@ -38,9 +40,6 @@ from awx.main.fields import JSONField from awx.main.utils import schedule_task_manager -from copy import copy -from urllib.parse import urljoin - __all__ = ['WorkflowJobTemplate', 'WorkflowJob', 'WorkflowJobOptions', 'WorkflowJobNode', 'WorkflowJobTemplateNode', 'WorkflowApprovalTemplate', 'WorkflowApproval'] @@ -712,6 +711,8 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin): def send_approval_notification(self, status): from awx.main.tasks import send_notifications # avoid circular import + if self.workflow_job_template is None: + return for nt in self.workflow_job_template.notification_templates["approvals"]: try: (notification_subject, notification_body) = self.build_notification_message(nt, status) @@ -727,24 +728,22 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin): def build_notification_message(self, nt, status): subject = [] - subject.append(('{}, Job Number {}').format(self.workflow_approval_template.name, - self.id)) + workflow_url = urljoin(settings.TOWER_URL_BASE, '/#/workflows/{}'.format(self.workflow_job.id)) + subject.append(('The approval node "{}"').format(self.workflow_approval_template.name)) if status == 'running': - subject.append('Please approve or deny this node.') + subject.append((' is running. You can approve or deny this node at: {}').format(workflow_url)) if status == 'approved': - subject.append('This approval node was approved.') + subject.append((' was approved. {}').format(workflow_url)) if status == 'timed_out': - subject.append('This approval node has timed out.') + subject.append((' has timed out. {}').format(workflow_url)) elif status == 'denied': - subject.append('This approval node was denied.') + subject.append((' was denied. {}').format(workflow_url)) + subject = " ".join(subject) body = self.notification_data() body['body'] = subject return subject, body - # def get_notification_templates(self): - # return self.workflow_job_template.notification_templates - @property def workflow_job_template(self): try: diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 818714998e..03bdd924d6 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -323,7 +323,7 @@ def send_notifications(notification_list, job_id=None): notification.status = "successful" notification.notifications_sent = sent except Exception as e: - logger.error("Send Notification Failed {}".format(e)) + logger.exception("Send Notification Failed {}".format(e)) notification.status = "failed" notification.error = smart_str(e) update_fields.append('error') From f835c8650be61c547a3c070fc9f25117e82e85c0 Mon Sep 17 00:00:00 2001 From: beeankha Date: Tue, 10 Sep 2019 12:30:23 -0400 Subject: [PATCH 07/15] Enable org-level approval notifications to work. --- awx/main/models/workflow.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index b736dc503f..0d4969aa17 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -442,6 +442,16 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl .filter(unifiedjobtemplate_notification_templates_for_success__in=[self])) approval_notification_templates = list(base_notification_templates .filter(unifiedjobtemplate_notification_templates_for_approvals__in=[self])) + # Get Organization NotificationTemplates + if self.organization is not None: + error_notification_templates = set(error_notification_templates + list(base_notification_templates.filter( + organization_notification_templates_for_errors=self.organization))) + started_notification_templates = set(started_notification_templates + list(base_notification_templates.filter( + organization_notification_templates_for_started=self.organization))) + success_notification_templates = set(success_notification_templates + list(base_notification_templates.filter( + organization_notification_templates_for_success=self.organization))) + approval_notification_templates = set(approval_notification_templates + list(base_notification_templates.filter( + organization_notification_templates_for_approvals=self.organization))) return dict(error=list(error_notification_templates), started=list(started_notification_templates), success=list(success_notification_templates), @@ -731,7 +741,7 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin): workflow_url = urljoin(settings.TOWER_URL_BASE, '/#/workflows/{}'.format(self.workflow_job.id)) subject.append(('The approval node "{}"').format(self.workflow_approval_template.name)) if status == 'running': - subject.append((' is running. You can approve or deny this node at: {}').format(workflow_url)) + subject.append((' is running. This node can be approved or denied at: {}').format(workflow_url)) if status == 'approved': subject.append((' was approved. {}').format(workflow_url)) if status == 'timed_out': From 17a8e08d93d5dbe13c111aa4b7805c2ab65a863f Mon Sep 17 00:00:00 2001 From: beeankha Date: Tue, 10 Sep 2019 14:36:12 -0400 Subject: [PATCH 08/15] Add unit tests for approval notifications --- .../functional/api/test_notifications.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/awx/main/tests/functional/api/test_notifications.py b/awx/main/tests/functional/api/test_notifications.py index 1a64220dfa..b7e9af9fcd 100644 --- a/awx/main/tests/functional/api/test_notifications.py +++ b/awx/main/tests/functional/api/test_notifications.py @@ -135,3 +135,45 @@ def test_search_on_notification_configuration_is_prevented(get, admin): response = get(url, {'notification_configuration__regex': 'ABCDEF'}, admin) assert response.status_code == 403 assert response.data == {"detail": "Filtering on notification_configuration is not allowed."} + + +@pytest.mark.django_db +def test_get_wfjt_approval_notification(get, admin, workflow_job_template): + url = reverse('api:workflow_job_template_notification_templates_approvals_list', kwargs={'pk': workflow_job_template.pk}) + response = get(url, admin) + assert response.status_code == 200 + assert len(response.data['results']) == 0 + + +@pytest.mark.django_db +def test_post_wfjt_approval_notification(get, post, admin, notification_template, workflow_job_template): + url = reverse('api:workflow_job_template_notification_templates_approvals_list', kwargs={'pk': workflow_job_template.pk}) + response = post(url, + dict(id=notification_template.id, + associate=True), + admin) + assert response.status_code == 204 + response = get(url, admin) + assert response.status_code == 200 + assert len(response.data['results']) == 1 + + +@pytest.mark.django_db +def test_get_org_approval_notification(get, admin, organization): + url = reverse('api:organization_notification_templates_approvals_list', kwargs={'pk': organization.pk}) + response = get(url, admin) + assert response.status_code == 200 + assert len(response.data['results']) == 0 + + +@pytest.mark.django_db +def test_post_org_approval_notification(get, post, admin, notification_template, organization): + url = reverse('api:organization_notification_templates_approvals_list', kwargs={'pk': organization.pk}) + response = post(url, + dict(id=notification_template.id, + associate=True), + admin) + assert response.status_code == 204 + response = get(url, admin) + assert response.status_code == 200 + assert len(response.data['results']) == 1 From 1ddf9fd1ed77627e434df5b767a86f16160b1dd6 Mon Sep 17 00:00:00 2001 From: beeankha Date: Wed, 11 Sep 2019 15:23:37 -0400 Subject: [PATCH 09/15] Fix up models, clean up code re: PR comments --- awx/api/serializers.py | 4 +--- .../0088_v360_approval_node_notifications.py | 6 +++--- awx/main/models/base.py | 7 ------- awx/main/models/notifications.py | 4 ++-- awx/main/models/organization.py | 5 +++++ awx/main/models/unified_jobs.py | 4 ---- awx/main/models/workflow.py | 20 ++++++++++--------- 7 files changed, 22 insertions(+), 28 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 11d31e8856..8100a78114 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -3492,9 +3492,7 @@ class WorkflowApprovalTemplateSerializer(UnifiedJobTemplateSerializer): if 'last_job' in res: del res['last_job'] - res.update(dict( - jobs = self.reverse('api:workflow_approval_template_jobs_list', kwargs={'pk': obj.pk}), - )) + res.update(jobs = self.reverse('api:workflow_approval_template_jobs_list', kwargs={'pk': obj.pk})) return res diff --git a/awx/main/migrations/0088_v360_approval_node_notifications.py b/awx/main/migrations/0088_v360_approval_node_notifications.py index d24051c18e..7c5d0d3062 100644 --- a/awx/main/migrations/0088_v360_approval_node_notifications.py +++ b/awx/main/migrations/0088_v360_approval_node_notifications.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.4 on 2019-09-04 18:23 +# Generated by Django 2.2.4 on 2019-09-11 13:44 from django.db import migrations, models @@ -16,9 +16,9 @@ class Migration(migrations.Migration): field=models.ManyToManyField(blank=True, related_name='organization_notification_templates_for_approvals', to='main.NotificationTemplate'), ), migrations.AddField( - model_name='unifiedjobtemplate', + model_name='workflowjobtemplate', name='notification_templates_approvals', - field=models.ManyToManyField(blank=True, related_name='unifiedjobtemplate_notification_templates_for_approvals', to='main.NotificationTemplate'), + field=models.ManyToManyField(blank=True, related_name='workflowjobtemplate_notification_templates_for_approvals', to='main.NotificationTemplate'), ), migrations.AlterField( model_name='workflowjobnode', diff --git a/awx/main/models/base.py b/awx/main/models/base.py index 97aa94f1d8..70fa92cf0a 100644 --- a/awx/main/models/base.py +++ b/awx/main/models/base.py @@ -392,13 +392,6 @@ class NotificationFieldsModel(BaseModel): related_name='%(class)s_notification_templates_for_started' ) - # Placeholder, unsure if this is required... - notification_templates_approvals = models.ManyToManyField( - "NotificationTemplate", - blank=True, - related_name='%(class)s_notification_templates_for_approvals' - ) - def prevent_search(relation): """ diff --git a/awx/main/models/notifications.py b/awx/main/models/notifications.py index 2d2a692824..ef428bcdfa 100644 --- a/awx/main/models/notifications.py +++ b/awx/main/models/notifications.py @@ -463,8 +463,8 @@ class JobNotificationMixin(object): def send_notification_templates(self, status): from awx.main.tasks import send_notifications # avoid circular import - if status not in ['pending', 'running', 'succeeded', 'failed']: - raise ValueError(_("status must be either pending, running, succeeded or failed")) + if status not in ['running', 'succeeded', 'failed']: + raise ValueError(_("status must be either running, succeeded or failed")) try: notification_templates = self.get_notification_templates() except Exception: diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index 60505b6e0a..df5d491d20 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -51,6 +51,11 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi default=0, help_text=_('Maximum number of hosts allowed to be managed by this organization.'), ) + notification_templates_approvals = models.ManyToManyField( + "NotificationTemplate", + blank=True, + related_name='%(class)s_notification_templates_for_approvals' + ) admin_role = ImplicitRoleField( parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index d4fe22806c..b8dd1d1cf8 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -1217,10 +1217,6 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique status=self.status, traceback=self.result_traceback) - # Placeholder... - # def approval_notification_data(self): - # for approval in WorkflowApproval.objects.filter(workflow_approval_template=instance, status='pending'): - def pre_start(self, **kwargs): if not self.can_start: self.job_explanation = u'%s is not in a startable state: %s, expecting one of %s' % (self._meta.verbose_name, self.status, str(('new', 'waiting'))) diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index 0d4969aa17..59d7ae7f1c 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -387,6 +387,12 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl blank=True, default=False, ) + notification_templates_approvals = models.ManyToManyField( + "NotificationTemplate", + blank=True, + related_name='%(class)s_notification_templates_for_approvals' + ) + admin_role = ImplicitRoleField(parent_role=[ 'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, 'organization.workflow_admin_role' @@ -441,15 +447,9 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl success_notification_templates = list(base_notification_templates .filter(unifiedjobtemplate_notification_templates_for_success__in=[self])) approval_notification_templates = list(base_notification_templates - .filter(unifiedjobtemplate_notification_templates_for_approvals__in=[self])) + .filter(workflowjobtemplate_notification_templates_for_approvals__in=[self])) # Get Organization NotificationTemplates if self.organization is not None: - error_notification_templates = set(error_notification_templates + list(base_notification_templates.filter( - organization_notification_templates_for_errors=self.organization))) - started_notification_templates = set(started_notification_templates + list(base_notification_templates.filter( - organization_notification_templates_for_started=self.organization))) - success_notification_templates = set(success_notification_templates + list(base_notification_templates.filter( - organization_notification_templates_for_success=self.organization))) approval_notification_templates = set(approval_notification_templates + list(base_notification_templates.filter( organization_notification_templates_for_approvals=self.organization))) return dict(error=list(error_notification_templates), @@ -726,9 +726,11 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin): for nt in self.workflow_job_template.notification_templates["approvals"]: try: (notification_subject, notification_body) = self.build_notification_message(nt, status) - except AttributeError: - raise NotImplementedError("build_notification_message() does not exist" % status) + except Exception: + logger.debug("build_notification_message() does not exist") + # Use kwargs to force late-binding + # https://stackoverflow.com/a/3431699/10669572 def send_it(local_nt=nt, local_subject=notification_subject, local_body=notification_body): def _func(): send_notifications.delay([local_nt.generate_notification(local_subject, local_body).id], From 8eb14841296c5f703671a8a069850ec1870fd3a4 Mon Sep 17 00:00:00 2001 From: beeankha Date: Fri, 13 Sep 2019 14:15:01 -0400 Subject: [PATCH 10/15] Update migration file, change status syntax --- ... => 0090_v360_approval_node_notifications.py} | 2 +- awx/main/models/workflow.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) rename awx/main/migrations/{0088_v360_approval_node_notifications.py => 0090_v360_approval_node_notifications.py} (94%) diff --git a/awx/main/migrations/0088_v360_approval_node_notifications.py b/awx/main/migrations/0090_v360_approval_node_notifications.py similarity index 94% rename from awx/main/migrations/0088_v360_approval_node_notifications.py rename to awx/main/migrations/0090_v360_approval_node_notifications.py index 7c5d0d3062..8b2eb43feb 100644 --- a/awx/main/migrations/0088_v360_approval_node_notifications.py +++ b/awx/main/migrations/0090_v360_approval_node_notifications.py @@ -6,7 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('main', '0087_v360_update_credential_injector_help_text'), + ('main', '0089_v360_new_job_event_types'), ] operations = [ diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index 59d7ae7f1c..29816be755 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -719,15 +719,15 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin): self.send_approval_notification('running') return can_start - def send_approval_notification(self, status): + def send_approval_notification(self, approval_status): from awx.main.tasks import send_notifications # avoid circular import if self.workflow_job_template is None: return for nt in self.workflow_job_template.notification_templates["approvals"]: try: - (notification_subject, notification_body) = self.build_notification_message(nt, status) + (notification_subject, notification_body) = self.build_approval_notification_message(nt, approval_status) except Exception: - logger.debug("build_notification_message() does not exist") + raise NotImplementedError("build_approval_notification_message() does not exist") # Use kwargs to force late-binding # https://stackoverflow.com/a/3431699/10669572 @@ -738,17 +738,17 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin): return _func connection.on_commit(send_it()) - def build_notification_message(self, nt, status): + def build_approval_notification_message(self, nt, approval_status): subject = [] workflow_url = urljoin(settings.TOWER_URL_BASE, '/#/workflows/{}'.format(self.workflow_job.id)) subject.append(('The approval node "{}"').format(self.workflow_approval_template.name)) - if status == 'running': + if approval_status == 'running': subject.append((' is running. This node can be approved or denied at: {}').format(workflow_url)) - if status == 'approved': + if approval_status == 'approved': subject.append((' was approved. {}').format(workflow_url)) - if status == 'timed_out': + if approval_status == 'timed_out': subject.append((' has timed out. {}').format(workflow_url)) - elif status == 'denied': + elif approval_status == 'denied': subject.append((' was denied. {}').format(workflow_url)) subject = " ".join(subject) body = self.notification_data() From ce6a276e1f8d08f121774f3fcdb3c96e5863cff4 Mon Sep 17 00:00:00 2001 From: beeankha Date: Tue, 17 Sep 2019 16:38:48 -0400 Subject: [PATCH 11/15] Update migration file --- ...otifications.py => 0091_v360_approval_node_notifications.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename awx/main/migrations/{0090_v360_approval_node_notifications.py => 0091_v360_approval_node_notifications.py} (95%) diff --git a/awx/main/migrations/0090_v360_approval_node_notifications.py b/awx/main/migrations/0091_v360_approval_node_notifications.py similarity index 95% rename from awx/main/migrations/0090_v360_approval_node_notifications.py rename to awx/main/migrations/0091_v360_approval_node_notifications.py index 8b2eb43feb..3963b776b8 100644 --- a/awx/main/migrations/0090_v360_approval_node_notifications.py +++ b/awx/main/migrations/0091_v360_approval_node_notifications.py @@ -6,7 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('main', '0089_v360_new_job_event_types'), + ('main', '0090_v360_WFJT_prompts'), ] operations = [ From 96689f45c8d6cca7a8aa929f00aa8987ea6722f4 Mon Sep 17 00:00:00 2001 From: beeankha Date: Wed, 18 Sep 2019 18:14:31 -0400 Subject: [PATCH 12/15] Update approval notification message --- awx/main/models/workflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index 29816be755..a278b5b9c7 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -743,7 +743,7 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin): workflow_url = urljoin(settings.TOWER_URL_BASE, '/#/workflows/{}'.format(self.workflow_job.id)) subject.append(('The approval node "{}"').format(self.workflow_approval_template.name)) if approval_status == 'running': - subject.append((' is running. This node can be approved or denied at: {}').format(workflow_url)) + subject.append((' needs review. This node can be viewed at: {}').format(workflow_url)) if approval_status == 'approved': subject.append((' was approved. {}').format(workflow_url)) if approval_status == 'timed_out': From 5aa6a94710ae960b4079db9069a12466e827aeb6 Mon Sep 17 00:00:00 2001 From: beeankha Date: Thu, 19 Sep 2019 11:12:19 -0400 Subject: [PATCH 13/15] Enable approval notifications to show up at... ...workflow jobs notifications endpoint --- awx/api/generics.py | 5 ++++- awx/api/views/__init__.py | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/awx/api/generics.py b/awx/api/generics.py index 06f80887ae..be58b057d8 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -489,9 +489,12 @@ class SubListAPIView(ParentMixin, ListAPIView): parent = self.get_parent_object() self.check_parent_access(parent) qs = self.request.user.get_queryset(self.model).distinct() - sublist_qs = getattrd(parent, self.relationship).distinct() + sublist_qs = self.get_sublist_queryset(parent) return qs & sublist_qs + def get_sublist_queryset(self, parent): + return getattrd(parent, self.relationship).distinct() + class DestroyAPIView(generics.DestroyAPIView): diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 90253c448f..7b831edfa5 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -3379,6 +3379,11 @@ class WorkflowJobNotificationsList(SubListAPIView): relationship = 'notifications' search_fields = ('subject', 'notification_type', 'body',) + def get_sublist_queryset(self, parent): + return self.model.objects.filter(Q(unifiedjob_notifications=parent) | + Q(unifiedjob_notifications__unified_job_node__workflow_job=parent, + unifiedjob_notifications__workflowapproval__isnull=False)).distinct() + class WorkflowJobActivityStreamList(SubListAPIView): From 9ed4e1682d273a53d74dc7354ffbe8cebedad3fd Mon Sep 17 00:00:00 2001 From: beeankha Date: Mon, 23 Sep 2019 15:40:39 -0400 Subject: [PATCH 14/15] Remove redundant whitespace --- awx/main/models/workflow.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index a278b5b9c7..29715d4cc8 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -743,13 +743,13 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin): workflow_url = urljoin(settings.TOWER_URL_BASE, '/#/workflows/{}'.format(self.workflow_job.id)) subject.append(('The approval node "{}"').format(self.workflow_approval_template.name)) if approval_status == 'running': - subject.append((' needs review. This node can be viewed at: {}').format(workflow_url)) + subject.append(('needs review. This node can be viewed at: {}').format(workflow_url)) if approval_status == 'approved': - subject.append((' was approved. {}').format(workflow_url)) + subject.append(('was approved. {}').format(workflow_url)) if approval_status == 'timed_out': - subject.append((' has timed out. {}').format(workflow_url)) + subject.append(('has timed out. {}').format(workflow_url)) elif approval_status == 'denied': - subject.append((' was denied. {}').format(workflow_url)) + subject.append(('was denied. {}').format(workflow_url)) subject = " ".join(subject) body = self.notification_data() body['body'] = subject From 3182197287d27c94261828e0e5b0e070f66ac25c Mon Sep 17 00:00:00 2001 From: mabashian Date: Fri, 27 Sep 2019 15:40:57 -0400 Subject: [PATCH 15/15] Makes notification toggles more responsive on smaller screens. --- .../src/notifications/notifications.list.js | 15 ++++++++------- .../shared/column-sort/column-sort.partial.html | 2 +- .../list-generator/list-generator.factory.js | 1 + 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/awx/ui/client/src/notifications/notifications.list.js b/awx/ui/client/src/notifications/notifications.list.js index a08dd7692e..756588d200 100644 --- a/awx/ui/client/src/notifications/notifications.list.js +++ b/awx/ui/client/src/notifications/notifications.list.js @@ -25,19 +25,20 @@ export default ['i18n', 'templateUrl', function(i18n, templateUrl){ name: { key: true, label: i18n._('Name'), - columnClass: 'col-md-4 col-sm-9 col-xs-9', - linkTo: '/#/notification_templates/{{notification.id}}' + columnClass: 'col-sm-9 col-xs-9', + linkTo: '/#/notification_templates/{{notification.id}}', + columnNgClass: "{'col-lg-4 col-md-2': showApprovalColumn, 'col-lg-5 col-md-3': !showApprovalColumn}" }, notification_type: { label: i18n._('Type'), searchType: 'select', searchOptions: [], excludeModal: true, - columnClass: 'd-none d-sm-flex col-md-4 col-sm-3' + columnClass: 'd-none d-sm-flex col-lg-4 col-md-2 col-sm-3', }, notification_templates_approvals: { label: i18n._('Approval'), - columnClass: 'd-none d-md-flex justify-content-start col-md-1', + columnClass: 'd-none d-md-flex justify-content-start col-lg-1 col-md-2', flag: 'notification_templates_approvals', type: "toggle", ngClick: "toggleNotification($event, notification.id, 'notification_templates_approvals')", @@ -58,7 +59,7 @@ export default ['i18n', 'templateUrl', function(i18n, templateUrl){ dataTipWatch: "schedule.play_tip", dataPlacement: "right", nosort: true, - columnClass: 'd-none d-md-flex justify-content-start col-md-1' + columnClass: 'd-none d-md-flex justify-content-start col-lg-1 col-md-2' }, notification_templates_success: { label: i18n._('Success'), @@ -70,11 +71,11 @@ export default ['i18n', 'templateUrl', function(i18n, templateUrl){ dataTipWatch: "schedule.play_tip", dataPlacement: "right", nosort: true, - columnClass: 'd-none d-md-flex justify-content-start col-md-1' + columnClass: 'd-none d-md-flex justify-content-start col-lg-1 col-md-2' }, notification_templates_error: { label: i18n._('Failure'), - columnClass: 'd-none d-md-flex justify-content-start col-md-1 NotifierList-lastColumn', + columnClass: 'd-none d-md-flex justify-content-start col-lg-1 col-md-2 NotifierList-lastColumn', flag: 'notification_templates_error', type: "toggle", ngClick: "toggleNotification($event, notification.id, 'notification_templates_error')", diff --git a/awx/ui/client/src/shared/column-sort/column-sort.partial.html b/awx/ui/client/src/shared/column-sort/column-sort.partial.html index c250ad4571..2efe6d8db5 100644 --- a/awx/ui/client/src/shared/column-sort/column-sort.partial.html +++ b/awx/ui/client/src/shared/column-sort/column-sort.partial.html @@ -1,4 +1,4 @@ -
+
{{columnLabel | translate}}
diff --git a/awx/ui/client/src/shared/list-generator/list-generator.factory.js b/awx/ui/client/src/shared/list-generator/list-generator.factory.js index b63fa96f9b..3b0b15e32c 100644 --- a/awx/ui/client/src/shared/list-generator/list-generator.factory.js +++ b/awx/ui/client/src/shared/list-generator/list-generator.factory.js @@ -558,6 +558,7 @@ export default ['$compile', 'Attr', 'Icon', column-no-sort="${list.fields[fld].nosort}" column-label="${list.fields[fld].label}" column-custom-class="${customClass}" + ng-class="${list.fields[fld].columnNgClass || `{'list-header-noSort': ${list.fields[fld].nosort ? true : false}}`}" query-set="${list.iterator}_queryset">
`; }