diff --git a/awx/api/serializers.py b/awx/api/serializers.py index f526fcfdc7..d4ab2811e6 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -3099,7 +3099,7 @@ class JobRelaunchSerializer(BaseSerializer): attrs = super(JobRelaunchSerializer, self).validate(attrs) return attrs - +# &&&&&& class JobCreateScheduleSerializer(BaseSerializer): can_schedule = serializers.SerializerMethodField() @@ -3437,7 +3437,7 @@ class WorkflowApprovalTemplateSerializer(UnifiedJobTemplateSerializer): class Meta: model = WorkflowApprovalTemplate - fields = ('*',) + fields = ('*', 'timeout', 'name',) def get_related(self, obj): res = super(WorkflowApprovalTemplateSerializer, self).get_related(obj) @@ -3453,6 +3453,15 @@ class WorkflowApprovalTemplateSerializer(UnifiedJobTemplateSerializer): return res +# class WorkflowJobTemplateApprovalSerializer(UnifiedJobTemplateSerializer): +# class Meta: +# model = WorkflowJobTemplateApproval +# fields = ('*',) +# +# def post(self, obj): +# return # POST only!!! + + class LaunchConfigurationBaseSerializer(BaseSerializer): scm_branch = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None) job_type = serializers.ChoiceField(allow_blank=True, allow_null=True, required=False, default=None, @@ -3592,6 +3601,7 @@ class WorkflowJobTemplateNodeSerializer(LaunchConfigurationBaseSerializer): def get_related(self, obj): res = super(WorkflowJobTemplateNodeSerializer, self).get_related(obj) + res['create_approval_job_template'] = self.reverse('api:workflow_job_template_node_create_approval', kwargs={'pk': obj.pk}) res['success_nodes'] = self.reverse('api:workflow_job_template_node_success_nodes_list', kwargs={'pk': obj.pk}) res['failure_nodes'] = self.reverse('api:workflow_job_template_node_failure_nodes_list', kwargs={'pk': obj.pk}) res['always_nodes'] = self.reverse('api:workflow_job_template_node_always_nodes_list', kwargs={'pk': obj.pk}) @@ -3660,6 +3670,13 @@ class WorkflowJobTemplateNodeDetailSerializer(WorkflowJobTemplateNodeSerializer) field_kwargs.pop('queryset', None) return field_class, field_kwargs +# &&&&&& +class WorkflowJobTemplateNodeCreateApprovalSerializer(BaseSerializer): + + class Meta: + model = WorkflowApprovalTemplate + fields = ('timeout', 'name', 'description',) + class JobListSerializer(JobSerializer, UnifiedJobListSerializer): pass diff --git a/awx/api/urls/urls.py b/awx/api/urls/urls.py index 97819b5ce9..beaa7532c7 100644 --- a/awx/api/urls/urls.py +++ b/awx/api/urls/urls.py @@ -133,8 +133,8 @@ v2_urls = [ url(r'^unified_job_templates/$', UnifiedJobTemplateList.as_view(), name='unified_job_template_list'), url(r'^unified_jobs/$', UnifiedJobList.as_view(), name='unified_job_list'), url(r'^activity_stream/', include(activity_stream_urls)), - url(r'^workflow_approval_templates/', include(workflow_approval_template_urls)), - url(r'^workflow_approval/', include(workflow_approval_urls)), + url(r'^workflow_approval_templates/', include(workflow_approval_template_urls)), # &&&&&& Take this line out completely? + url(r'^workflow_approvals/', include(workflow_approval_urls)), ] diff --git a/awx/api/urls/workflow_job_template_node.py b/awx/api/urls/workflow_job_template_node.py index 14cb49137e..76ba375cb7 100644 --- a/awx/api/urls/workflow_job_template_node.py +++ b/awx/api/urls/workflow_job_template_node.py @@ -10,6 +10,7 @@ from awx.api.views import ( WorkflowJobTemplateNodeFailureNodesList, WorkflowJobTemplateNodeAlwaysNodesList, WorkflowJobTemplateNodeCredentialsList, + WorkflowJobTemplateNodeCreateApproval, ) @@ -20,6 +21,7 @@ urls = [ url(r'^(?P[0-9]+)/failure_nodes/$', WorkflowJobTemplateNodeFailureNodesList.as_view(), name='workflow_job_template_node_failure_nodes_list'), url(r'^(?P[0-9]+)/always_nodes/$', WorkflowJobTemplateNodeAlwaysNodesList.as_view(), name='workflow_job_template_node_always_nodes_list'), url(r'^(?P[0-9]+)/credentials/$', WorkflowJobTemplateNodeCredentialsList.as_view(), name='workflow_job_template_node_credentials_list'), + url(r'^(?P[0-9]+)/create_approval_job_template/$', WorkflowJobTemplateNodeCreateApproval.as_view(), name='workflow_job_template_node_create_approval'), ] __all__ = ['urls'] diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 58fff401c1..8fa5bbff45 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -3013,6 +3013,21 @@ class WorkflowJobTemplateNodeChildrenBaseList(EnforceParentRelationshipMixin, Su return None +class WorkflowJobTemplateNodeCreateApproval(RetrieveAPIView): + + model = models.WorkflowJobTemplateNode + serializer_class = serializers.WorkflowJobTemplateNodeCreateApprovalSerializer + +# &&&&&& + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + obj = self.get_object() + approval_template = obj.create_approval_template(**serializer.validated_data) + return Response(data={'id':approval_template.pk}, status=status.HTTP_200_OK) + + class WorkflowJobTemplateNodeSuccessNodesList(WorkflowJobTemplateNodeChildrenBaseList): relationship = 'success_nodes' @@ -3582,7 +3597,7 @@ class JobRelaunch(RetrieveAPIView): headers = {'Location': new_job.get_absolute_url(request=request)} return Response(data, status=status.HTTP_201_CREATED, headers=headers) - +# &&&&&& Reference class JobCreateSchedule(RetrieveAPIView): model = models.Job @@ -4466,7 +4481,7 @@ class WorkflowApprovalDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView): model = models.WorkflowApproval serializer_class = serializers.WorkflowApprovalSerializer - +# &&&&&& Include checks in the below two post methods class WorkflowApprovalApprove(RetrieveAPIView): model = models.WorkflowApproval serializer_class = serializers.WorkflowApprovalViewSerializer diff --git a/awx/api/views/root.py b/awx/api/views/root.py index bf85b4866e..df5f6f7f0e 100644 --- a/awx/api/views/root.py +++ b/awx/api/views/root.py @@ -124,8 +124,8 @@ class ApiVersionRootView(APIView): data['activity_stream'] = reverse('api:activity_stream_list', request=request) data['workflow_job_templates'] = reverse('api:workflow_job_template_list', request=request) data['workflow_jobs'] = reverse('api:workflow_job_list', request=request) - data['workflow_approval_templates'] = reverse('api:workflow_approval_template_list', request=request) - data['workflow_approval'] = reverse('api:workflow_approval_list', request=request) + data['workflow_approval_templates'] = reverse('api:workflow_approval_template_list', request=request) # &&&&&& Take this line out completely? + data['workflow_approvals'] = reverse('api:workflow_approval_list', request=request) data['workflow_job_template_nodes'] = reverse('api:workflow_job_template_node_list', request=request) data['workflow_job_nodes'] = reverse('api:workflow_job_node_list', request=request) return Response(data) diff --git a/awx/main/access.py b/awx/main/access.py index bcd9cd1dab..5b7017d9f3 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -2792,24 +2792,32 @@ class WorkflowApprovalAccess(BaseAccess): def can_start(self, obj, validate_license=True): return False + # &&&&&& ??? Start of the RBAC method ??? + # def can_approve_or_deny(self, obj): + # if self.user.is_superuser: # &&&&&& add "or self.user.approval_role"? + # return True + # return self.can_change(obj, ????) +# &&&&&& Why is the below not showing up as a class now?? class WorkflowApprovalTemplateAccess(BaseAccess): ''' + I can create approval nodes when: + - I can approve workflows when: - - I'm authenticated - I can create when: - - I'm a superuser: + - ''' model = WorkflowApprovalTemplate prefetch_related = ('created_by', 'modified_by',) - def can_read(self, obj): - return True - - def can_use(self, obj): - return True + # &&&&&& I need to get the admin role of the WFJT, where WFJT is provided in the key portion vs the data (Alan said that, what does it mean exactly???) + @check_superuser + def can_add(self, data): + if data is None: # Hide direct creation in API browser + return False + return ( + self.check_related('workflow_approval_template', UnifiedJobTemplate, role_field='admin_role') def filtered_queryset(self): return self.model.filter(workflowjobtemplatenodes__workflow_job_template=WorkflowJobTemplate.accessible_pk_qs(self.user, 'read_role')) diff --git a/awx/main/fields.py b/awx/main/fields.py index d0286f553a..d395803c7c 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -164,7 +164,7 @@ def is_implicit_parent(parent_role, child_role): # The only singleton implicit parent is the system admin being # a parent of the system auditor role return bool( - child_role.singleton_name == ROLE_SINGLETON_SYSTEM_AUDITOR and + child_role.singleton_name == ROLE_SINGLETON_SYSTEM_AUDITOR and parent_role.singleton_name == ROLE_SINGLETON_SYSTEM_ADMINISTRATOR ) # Get the list of implicit parents that were defined at the class level. diff --git a/awx/main/management/commands/cleanup_jobs.py b/awx/main/management/commands/cleanup_jobs.py index 81f405da4c..f43a34c6b6 100644 --- a/awx/main/management/commands/cleanup_jobs.py +++ b/awx/main/management/commands/cleanup_jobs.py @@ -200,6 +200,8 @@ class Command(BaseCommand): skipped += WorkflowJob.objects.filter(created__gte=self.cutoff).count() return skipped, deleted +# &&&&&& Add cleanup of orphaned approval nodes here? + def cleanup_notifications(self): skipped, deleted = 0, 0 notifications = Notification.objects.filter(created__lt=self.cutoff) diff --git a/awx/main/migrations/0082_v360_workflowapproval.py b/awx/main/migrations/0082_v360_workflowapproval.py deleted file mode 100644 index 80a188c274..0000000000 --- a/awx/main/migrations/0082_v360_workflowapproval.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.20 on 2019-07-03 14:38 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0081_v360_notify_on_start'), - ] - - operations = [ - migrations.CreateModel( - name='WorkflowApproval', - fields=[ - ('unifiedjob_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='main.UnifiedJob')), - ], - options={ - 'manager_inheritance_from_future': True, - }, - bases=('main.unifiedjob',), - ), - migrations.CreateModel( - name='WorkflowApprovalTemplate', - fields=[ - ('unifiedjobtemplate_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='main.UnifiedJobTemplate')), - ], - options={ - 'manager_inheritance_from_future': True, - }, - bases=('main.unifiedjobtemplate',), - ), - migrations.AddField( - model_name='workflowapproval', - name='workflow_approval_template', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='approvals', to='main.WorkflowApprovalTemplate'), - ), - ] diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index a85653e112..627578a854 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -174,7 +174,7 @@ def o_auth2_token_get_absolute_url(self, request=None): OAuth2AccessToken.add_to_class('get_absolute_url', o_auth2_token_get_absolute_url) - +# &&&&&& "Add model here" - Alan from awx.main.registrar import activity_stream_registrar # noqa activity_stream_registrar.connect(Organization) activity_stream_registrar.connect(Inventory) @@ -202,6 +202,8 @@ activity_stream_registrar.connect(User) activity_stream_registrar.connect(WorkflowJobTemplate) activity_stream_registrar.connect(WorkflowJobTemplateNode) activity_stream_registrar.connect(WorkflowJob) +# activity_stream_registrar.connect(WorkflowApproval) &&&&&& +# activity_stream_registrar.connect(WorkflowApprovalTemplate) activity_stream_registrar.connect(OAuth2Application) activity_stream_registrar.connect(OAuth2AccessToken) diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index 47176f2550..d63ec5eb5d 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -483,4 +483,3 @@ class RelatedJobsMixin(object): raise RuntimeError("Programmer error. Expected _get_active_jobs() to return a QuerySet.") return [dict(id=t[0], type=mapping[t[1]]) for t in jobs.values_list('id', 'polymorphic_ctype_id')] - diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index 1c77c9e5be..21fb0fd7d1 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -89,6 +89,10 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi 'notification_admin_role', 'credential_admin_role', 'job_template_admin_role',], ) +# &&&&&& The below keeps complaining - fixed by new migration file, perhaps? + approval_role = ImplicitRoleField( + parent_role='admin_role', + ) def get_absolute_url(self, request=None): diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index a0b5b6785f..39a2109ad5 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -48,6 +48,7 @@ role_names = { 'read_role': _('Read'), 'update_role': _('Update'), 'use_role': _('Use'), + 'approval_role': _('Approve'), # &&&&&& Added this here! } role_descriptions = { @@ -70,6 +71,7 @@ role_descriptions = { 'read_role': _('May view settings for the %s'), 'update_role': _('May update the %s'), 'use_role': _('Can use the %s in a job template'), + 'approval_role': _('Can approve or deny a workflow approval node'), # &&&&&& ...and here! } @@ -480,7 +482,7 @@ def get_roles_on_resource(resource, accessor): ).values_list('role_field', flat=True).distinct() ] - +# &&&&&& This area is giving trouble? def role_summary_fields_generator(content_object, role_field): global role_descriptions global role_names diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index eabfcb9133..a0948e36b9 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -162,6 +162,13 @@ class WorkflowJobTemplateNode(WorkflowNodeBase): new_node.credentials.add(cred) return new_node + def create_approval_template(self, **kwargs): + approval_template = WorkflowApprovalTemplate(**kwargs) + approval_template.save() + self.unified_job_template = approval_template + self.save() + return approval_template + class WorkflowJobNode(WorkflowNodeBase): job = models.OneToOneField( @@ -388,6 +395,11 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl 'singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, 'organization.auditor_role', 'execute_role', 'admin_role' ]) +# &&&&&& The below keeps complaining - fixed by new migration file, perhaps? + approval_role = ImplicitRoleField(parent_role=[ + 'singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, + 'organization.approval_role', 'admin_role', + ]) @property def workflow_nodes(self): @@ -608,6 +620,12 @@ class WorkflowApprovalTemplate(UnifiedJobTemplate): class Meta: app_label = 'main' + timeout = models.IntegerField( + blank=True, + default=0, + help_text=_("The amount of time (in seconds) before the approval node expires and fails."), + ) + @classmethod def _get_unified_job_class(cls): return WorkflowApproval @@ -619,6 +637,28 @@ 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): + # # Return all notification_templates defined on the Job Template, on the Project, and on the Organization for each trigger type + # base_notification_templates = NotificationTemplate.objects + # error_notification_templates = list(base_notification_templates.filter( + # unifiedjobtemplate_notification_templates_for_errors__in=[self, self.project])) + # started_notification_templates = list(base_notification_templates.filter( + # unifiedjobtemplate_notification_templates_for_started__in=[self, self.project])) + # success_notification_templates = list(base_notification_templates.filter( + # unifiedjobtemplate_notification_templates_for_success__in=[self, self.project])) +# &&&&&& Approvals don't have orgs! How to pull them in? Alan said to "get creative"! + # if self.project is not None and self.project.organization is not 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), + # started=list(started_notification_templates), + # success=list(success_notification_templates)) + class WorkflowApproval(UnifiedJob): class Meta: diff --git a/awx/main/registrar.py b/awx/main/registrar.py index 6d0ccfe495..f7f32d839b 100644 --- a/awx/main/registrar.py +++ b/awx/main/registrar.py @@ -3,7 +3,7 @@ from django.db.models.signals import pre_save, post_save, pre_delete, m2m_changed - +# &&&&&& Where the signals are hooked up ?? class ActivityStreamRegistrar(object): def __init__(self): diff --git a/awx/main/signals.py b/awx/main/signals.py index da2898e43f..fa50b97fe9 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -355,6 +355,26 @@ def update_host_last_job_after_job_deleted(sender, **kwargs): for host in Host.objects.filter(pk__in=hosts_pks): _update_host_last_jhs(host) + +# &&&&&& Argh. Which one looks better? +# @receiver(pre_delete, sender=Job) +# def delete_detached_approval_nodes(sender, instance, **kwargs): +# for l in instance.labels.all(): +# if l.is_candidate_for_detach(): +# l.delete() +# +# +# @receiver(pre_delete, sender=Organization) +# def delete_detached_approval_nodes(sender, instance, **kwargs): +# approval_node = ??? +# user = get_current_user_or_none() +# for node in approval_node: +# try: +# node.schedule_deletion(user_id=getattr(user, 'id', None)) +# except RuntimeError as e: +# logger.debug(e) + + # Set via ActivityStreamRegistrar to record activity stream events @@ -434,7 +454,7 @@ def model_serializer_mapping(): models.OAuth2Application: serializers.OAuth2ApplicationSerializer, } - +# &&&&&& Can customize how/what the activity stream shows info def activity_stream_create(sender, instance, created, **kwargs): if created and activity_stream_enabled: # TODO: remove deprecated_group conditional in 3.3 @@ -462,6 +482,8 @@ def activity_stream_create(sender, instance, created, **kwargs): object1=object1, changes=json.dumps(changes), actor=get_current_user_or_none()) + # if type(instance) == WorkflowApproval: &&&&&& + # changes['status'] = #??? #TODO: Weird situation where cascade SETNULL doesn't work # it might actually be a good idea to remove all of these FK references since # we don't really use them anyway. diff --git a/awx/ui/client/lib/components/approvalsDrawer/approvalsDrawer.directive.js b/awx/ui/client/lib/components/approvalsDrawer/approvalsDrawer.directive.js index 0f6136a516..b215f95620 100644 --- a/awx/ui/client/lib/components/approvalsDrawer/approvalsDrawer.directive.js +++ b/awx/ui/client/lib/components/approvalsDrawer/approvalsDrawer.directive.js @@ -26,7 +26,7 @@ function AtApprovalsDrawerController (strings, Rest, GetBasePath, $rootScope) { const loadTheList = () => { const queryParams = Object.keys(vm.queryset).map(key => `${key}=${vm.queryset[key]}`).join('&'); - Rest.setUrl(`${GetBasePath('workflow_approval')}?${queryParams}`); + Rest.setUrl(`${GetBasePath('workflow_approvals')}?${queryParams}`); return Rest.get() .then(({ data }) => { vm.dataset = data; @@ -40,13 +40,13 @@ function AtApprovalsDrawerController (strings, Rest, GetBasePath, $rootScope) { .then(() => { vm.listLoaded = true; }); vm.approve = (approval) => { - Rest.setUrl(`${GetBasePath('workflow_approval')}${approval.id}/approve`); + Rest.setUrl(`${GetBasePath('workflow_approvals')}${approval.id}/approve`); Rest.post() .then(() => loadTheList()); }; vm.deny = (approval) => { - Rest.setUrl(`${GetBasePath('workflow_approval')}${approval.id}/deny`); + Rest.setUrl(`${GetBasePath('workflow_approvals')}${approval.id}/deny`); Rest.post() .then(() => loadTheList()); }; diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index 5c94436659..0e3cf2c9b2 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -388,7 +388,7 @@ angular }); }); - Rest.setUrl(`${GetBasePath('workflow_approval')}?status=pending&page_size=1`); + Rest.setUrl(`${GetBasePath('workflow_approvals')}?status=pending&page_size=1`); Rest.get() .then(({data}) => { $rootScope.pendingApprovalCount = data.count; diff --git a/awx/ui/client/src/login/loginModal/loginModal.controller.js b/awx/ui/client/src/login/loginModal/loginModal.controller.js index e5df9bf1b6..4cb975c29c 100644 --- a/awx/ui/client/src/login/loginModal/loginModal.controller.js +++ b/awx/ui/client/src/login/loginModal/loginModal.controller.js @@ -140,7 +140,7 @@ export default ['$log', '$cookies', '$rootScope', }); }); - Rest.setUrl(`${GetBasePath('workflow_approval')}?status=pending&page_size=1`); + Rest.setUrl(`${GetBasePath('workflow_approvals')}?status=pending&page_size=1`); Rest.get() .then(({data}) => { $rootScope.pendingApprovalCount = data.count; diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js index 72a74a5b8a..925c9a03fd 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js @@ -127,7 +127,7 @@ export default ['$scope', 'TemplatesService', if (_.has(node, 'fullUnifiedJobTemplateObject') && (node.fullUnifiedJobTemplateObject.type === "workflow_job_template" || - node.fullUnifiedJobTemplateObject.type === "job_template") && + node.fullUnifiedJobTemplateObject.type === "job_template") && node.promptData ) { sendableNodeData = PromptService.bundlePromptDataForSaving({ @@ -188,7 +188,7 @@ export default ['$scope', 'TemplatesService', return credFromPrompt.id === defaultCred.id; }); }); - + credentialIdsToPost.forEach((credentialToPost) => { credentialRequests.push({ id: data.id, @@ -309,7 +309,7 @@ export default ['$scope', 'TemplatesService', let disassociatePromises = []; let associatePromises = []; let linkMap = {}; - + // Build a link map for easy access $scope.graphState.arrayOfLinksForChart.forEach(link => { // link.source.id of 1 is our artificial start node @@ -319,11 +319,11 @@ export default ['$scope', 'TemplatesService', if (!linkMap[sourceNodeId]) { linkMap[sourceNodeId] = {}; } - + linkMap[sourceNodeId][targetNodeId] = link.edgeType; } }); - + Object.keys(nodeRef).map((workflowNodeId) => { let nodeId = nodeRef[workflowNodeId].originalNodeObject.id; if (nodeRef[workflowNodeId].originalNodeObject.success_nodes) { @@ -381,7 +381,7 @@ export default ['$scope', 'TemplatesService', }); } }); - + Object.keys(linkMap).map((sourceNodeId) => { Object.keys(linkMap[sourceNodeId]).map((targetNodeId) => { const sourceChartNodeId = nodeIdToChartNodeIdMapping[sourceNodeId]; @@ -432,7 +432,7 @@ export default ['$scope', 'TemplatesService', } }); }); - + $q.all(disassociatePromises) .then(() => { let credentialPromises = credentialRequests.map((request) => { @@ -441,7 +441,7 @@ export default ['$scope', 'TemplatesService', data: request.data }); }); - + return $q.all(associatePromises.concat(credentialPromises)) .then(() => { Wait('stop');