diff --git a/apps/web/src/app/workspaces/components/capability-request/capability-request.component.html b/apps/web/src/app/workspaces/components/capability-request/capability-request.component.html index dbbf9f31cde039f9a87fd81a5b38e39bd29a8f0d..edf32d4bdad2b3fda9c0bbffef2b09a3f8798a23 100644 --- a/apps/web/src/app/workspaces/components/capability-request/capability-request.component.html +++ b/apps/web/src/app/workspaces/components/capability-request/capability-request.component.html @@ -6,10 +6,10 @@ <div class="row"> <div id="capability-definition-container" class="col-5"> <span id="capability-label" - >Capability: + >Capability: <strong class="pl-1">{{ this.capabilityRequest.capability_name.toUpperCase() - }}</strong></span + }}</strong></span > <app-capability-definition *ngIf="this.capability !== undefined" @@ -29,7 +29,7 @@ id="download-button" class="btn btn-warning" href="{{ this.capabilityExecution.delivery_url }}" - >Download</a + >Download</a > </div> <div *ngIf="this.capabilityRequest.current_execution !== null"> @@ -46,11 +46,11 @@ href="/workflows/{{ this.capability.name }}/requests/{{ this.capabilityExecution.current_workflow_request_id }}/weblog" - >View Weblog</a + >View Weblog</a > </div> </div> - <br/> + <br /> <div id="qa-container" class="col-5"> <div class="d-flex justify-content-left py-3" @@ -62,16 +62,6 @@ " > <span id="qa-label">QA Processing: </span> - <button - id="set-qa-pass" - type="button" - class="btn btn-success" - (click)="passQa(this.capabilityExecution.current_workflow_request_id)" - > - <span class="fas fa-image"></span> - <span class="p1-2">Pass QA</span> - </button> - <div class="btn-group"> <div id="pass-button"> <button @@ -108,11 +98,16 @@ id="versions" class="pt-2" [capabilityRequest]="this.capabilityRequest" - (versionEvent) = setVersion($event) + [selected_version]="this.currentVersion" + (versionEvent)="setVersion($event)" ></app-versions> - <br/> - <span id="parameters-label" *ngIf="currentVersion !== undefined">Version {{currentVersion.version_number}} Parameters</span> - <span id="parameters-label" *ngIf="currentVersion === undefined">Version {{latestVersion.version_number}} Parameters</span> + <br /> + <span id="parameters-label" *ngIf="currentVersion !== undefined" + >Version {{ currentVersion.version_number }} Parameters</span + > + <span id="parameters-label" *ngIf="currentVersion === undefined" + >Version {{ latestVersion.version_number }} Parameters</span + > <app-parameters id="parameters" class="pt-2" @@ -125,9 +120,13 @@ [currentVersion]="latestVersion" *ngIf="currentVersion === undefined" ></app-parameters> - <br/> - <span id="files-label" *ngIf="currentVersion !== undefined">Version {{currentVersion.version_number}} Files</span> - <span id="files-label" *ngIf="currentVersion === undefined">Version {{latestVersion.version_number}} Files</span> + <br /> + <span id="files-label" *ngIf="currentVersion !== undefined" + >Version {{ currentVersion.version_number }} Files</span + > + <span id="files-label" *ngIf="currentVersion === undefined" + >Version {{ latestVersion.version_number }} Files</span + > <app-files id="files" class="pt-2" @@ -140,22 +139,21 @@ [capabilityVersion]="latestVersion" *ngIf="currentVersion === undefined" ></app-files> - <br/> + <br /> <app-metadata id="metadata" class="pt-2" [capabilityVersion]="currentVersion" - *ngIf=" - currentVersion !== undefined && currentVersion.workflow_metadata !== null - " + *ngIf="currentVersion !== undefined && currentVersion.workflow_metadata !== null" ></app-metadata> - <br/> + <br /> <app-request-operations id="operations" class="pt-2" [capability]="this.capability" [capabilityRequest]="this.capabilityRequest" + [currentVersion]="currentVersion" (cartaLaunched)="alertMessage($event)" ></app-request-operations> </div> diff --git a/apps/web/src/app/workspaces/components/capability-request/capability-request.component.ts b/apps/web/src/app/workspaces/components/capability-request/capability-request.component.ts index db275da0f12102c1c33e7ee56053bbc3c489270b..daa5e1b724dc57eaf72bf16fee03e88fc80deaa3 100644 --- a/apps/web/src/app/workspaces/components/capability-request/capability-request.component.ts +++ b/apps/web/src/app/workspaces/components/capability-request/capability-request.component.ts @@ -16,19 +16,20 @@ * You should have received a copy of the GNU General Public License * along with Workspaces. If not, see <https://www.gnu.org/licenses/>. */ -import {Component, OnDestroy, OnInit} from "@angular/core"; -import {CapabilityRequest} from "../../model/capability-request"; -import {ActivatedRoute} from "@angular/router"; -import {DataRetrieverService} from "../../services/data-retriever.service"; -import {CapabilityExecution} from "../../model/capability-execution"; -import {CapabilityRequestService} from "../../services/capability-request.service"; -import {CapabilityVersion} from "../../model/capability-version"; -import {PollingDataUpdaterService} from "../../services/polling-data-updater.service"; -import {AlertService} from "../../../shared/alert/alert.service"; -import {JsonObject} from "@angular/compiler-cli/ngcc/src/packages/entry_point"; -import {Subject} from "rxjs"; -import {takeUntil} from "rxjs/operators"; -import {Capability} from "../../model/capability"; +import { Component, OnDestroy, OnInit } from "@angular/core"; + +import { CapabilityRequest } from "../../model/capability-request"; +import { ActivatedRoute } from "@angular/router"; +import { DataRetrieverService } from "../../services/data-retriever.service"; +import { CapabilityExecution } from "../../model/capability-execution"; +import { CapabilityRequestService } from "../../services/capability-request.service"; +import { CapabilityVersion } from "../../model/capability-version"; +import { PollingDataUpdaterService } from "../../services/polling-data-updater.service"; +import { AlertService } from "../../../shared/alert/alert.service"; +import { JsonObject } from "@angular/compiler-cli/ngcc/src/packages/entry_point"; +import { Subject } from "rxjs"; +import { takeUntil } from "rxjs/operators"; +import { Capability } from "../../model/capability"; @Component({ selector: "app-capability-request", @@ -65,7 +66,6 @@ export class CapabilityRequestComponent implements OnInit, OnDestroy { type: "CapabilityExecution", }; } - this.dataRetriever .getCapability(this.capabilityRequest.capability_name) .pipe(takeUntil(this.ngUnsubscribe)) @@ -85,6 +85,14 @@ export class CapabilityRequestComponent implements OnInit, OnDestroy { next: (capabilityVersion) => { if (capabilityVersion !== undefined) { this.latestVersion = capabilityVersion; + // Set selected version to be the request's latest version + // If no version is selected or if the selected version is the latest version + if ( + this.currentVersion === undefined || + this.currentVersion.version_number === this.latestVersion.version_number + ) { + this.setVersion(this.latestVersion); + } } else { console.error("Current version returned undefined."); } @@ -173,12 +181,11 @@ export class CapabilityRequestComponent implements OnInit, OnDestroy { .subscribe(addRestCallObservable); } - setVersion(version: CapabilityVersion){ + setVersion(version: CapabilityVersion): void { this.currentVersion = version; } - ngOnInit(): void { - } + ngOnInit(): void {} ngOnDestroy(): void { // Prevent memory leakage by unsubscribing all observers diff --git a/apps/web/src/app/workspaces/components/capability-request/components/request-operations/request-operations.component.html b/apps/web/src/app/workspaces/components/capability-request/components/request-operations/request-operations.component.html index 454eb3a162a5d1269824fd2c87ed3fd8ddbaf1d4..e13051fdf3d4fd4c1edf3fedf269afc0bc287ae8 100644 --- a/apps/web/src/app/workspaces/components/capability-request/components/request-operations/request-operations.component.html +++ b/apps/web/src/app/workspaces/components/capability-request/components/request-operations/request-operations.component.html @@ -7,28 +7,46 @@ [capabilityRequest]="capabilityRequest" ></app-create-new-version-form> </div> - <div class="col-auto d-flex" *ngIf="capabilityRequest.state === 'Complete' && - (capabilityRequest.capability_name === 'std_calibration' || capabilityRequest.capability_name === 'restore_cms')"> + <div + class="col-auto d-flex" + *ngIf=" + capabilityRequest.state === 'Complete' && + (capabilityRequest.capability_name === 'std_calibration' || + capabilityRequest.capability_name === 'restore_cms') + " + > <button id="create-image-request" type="button" class="btn btn-warning" - (click)="capabilityLauncherService.createImageRequestFromPreviousCal('std_cms_imaging', capabilityRequest.id).subscribe(followonRequestObserver)"> + (click)=" + capabilityLauncherService + .createImageRequestFromPreviousCal('std_cms_imaging', capabilityRequest.id) + .subscribe(followonRequestObserver) + " + > <span class="fas fa-camera"></span><span class="pl-2">Create Image Request</span> </button> </div> - <div class="col-auto d-flex" *ngIf="capability !== undefined && capability.has_image_products === true && capabilityRequest.state === 'Complete'"> + <div + class="col-auto d-flex" + *ngIf=" + capability !== undefined && + capability.has_image_products === true && + capabilityRequest.state === 'Complete' + " + > <button id="launch-carta" type="button" class="btn btn-outline-success" (click)="launchCarta()" [disabled]="hasBeenClicked" - > + > <span class="fas fa-image"></span><span class="pl-2">Launch CARTA</span> </button> </div> - <div class="col-auto d-flex" *ngIf="capabilityRequest.state === 'Created'"> + <div class="col-auto d-flex" *ngIf="currentVersion.state === 'Created'"> <button id="submit-button" type="button" diff --git a/apps/web/src/app/workspaces/components/capability-request/components/request-operations/request-operations.component.ts b/apps/web/src/app/workspaces/components/capability-request/components/request-operations/request-operations.component.ts index 82912ae1a7e97b4f7c5637b611a106177bca3954..ab5cdfccc09e6471c9131a1823a4d72bf664a6ca 100644 --- a/apps/web/src/app/workspaces/components/capability-request/components/request-operations/request-operations.component.ts +++ b/apps/web/src/app/workspaces/components/capability-request/components/request-operations/request-operations.component.ts @@ -23,6 +23,7 @@ import { CapabilityLauncherService } from "../../../../services/capability-launc import { WorkflowLauncherService } from "../../../../services/workflow-launcher.service"; import { CapabilityExecution } from "../../../../model/capability-execution"; import { CapabilityRequestService } from "../../../../services/capability-request.service"; +import { CapabilityVersion } from "../../../../model/capability-version"; @Component({ selector: "app-request-operations", @@ -36,6 +37,7 @@ export class RequestOperationsComponent implements OnInit { private capabilityRequestService: CapabilityRequestService, ) {} @Input() capabilityRequest: CapabilityRequest; + @Input() currentVersion: CapabilityVersion; @Input() capability: Capability; public capabilityExecution: CapabilityExecution; @Output() cartaLaunched: EventEmitter<any> = new EventEmitter<any>(); diff --git a/apps/web/src/app/workspaces/components/capability-request/components/versions/versions.component.ts b/apps/web/src/app/workspaces/components/capability-request/components/versions/versions.component.ts index 4c5d484280b13ba95842d486e7868ff53c68649d..956d085594a5175ecf4750beb0449fe6ddd46db6 100644 --- a/apps/web/src/app/workspaces/components/capability-request/components/versions/versions.component.ts +++ b/apps/web/src/app/workspaces/components/capability-request/components/versions/versions.component.ts @@ -16,32 +16,28 @@ * You should have received a copy of the GNU General Public License * along with Workspaces. If not, see <https://www.gnu.org/licenses/>. */ -import {Component, Input, OnInit, Output, EventEmitter} from '@angular/core'; -import {CapabilityVersion} from "../../../../model/capability-version"; -import {CapabilityRequestService} from "../../../../services/capability-request.service"; -import {CapabilityRequest} from "../../../../model/capability-request"; -import {FormControl, FormGroup, Validators} from "@angular/forms"; -import {JsonObject} from "@angular/compiler-cli/ngcc/src/packages/entry_point"; +import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { CapabilityVersion } from "../../../../model/capability-version"; +import { CapabilityRequestService } from "../../../../services/capability-request.service"; +import { CapabilityRequest } from "../../../../model/capability-request"; +import { JsonObject } from "@angular/compiler-cli/ngcc/src/packages/entry_point"; @Component({ - selector: 'app-versions', - templateUrl: './versions.component.html', - styleUrls: ['./versions.component.scss'] + selector: "app-versions", + templateUrl: "./versions.component.html", + styleUrls: ["./versions.component.scss"], }) export class VersionsComponent implements OnInit { - @Input() capabilityRequest: CapabilityRequest; @Output() versionEvent = new EventEmitter<CapabilityVersion>(); public capabilityVersions: Array<CapabilityVersion>; - public selected_version: CapabilityVersion - + @Input() public selected_version: CapabilityVersion; constructor(private capabilityRequestService: CapabilityRequestService) { // this.getSpecificVersion(this.capabilityRequest.id, this.capabilityRequest.versions[this.capabilityRequest.versions.length-1].version_number); } - ngOnInit(): void { - } + ngOnInit(): void {} getVersion(): number { return this.selected_version.version_number; @@ -50,15 +46,15 @@ export class VersionsComponent implements OnInit { getAllVersions(requestId: string) { const versionsObserver = { next: (request) => { - this.capabilityVersions = request; - console.log(this.capabilityVersions) + this.capabilityVersions = request; + console.log(this.capabilityVersions); }, error: (error) => console.error("Error when retrieving capability versions:" + error), }; - this.capabilityRequestService.getAllVersions(requestId).subscribe(versionsObserver); + this.capabilityRequestService.getAllVersions(requestId).subscribe(versionsObserver); } - getSpecificVersion(request_id: string, version_id: number){ + getSpecificVersion(request_id: string, version_id: number) { const specificVersionObserver = { next: (version) => { this.selected_version = version; @@ -66,10 +62,11 @@ export class VersionsComponent implements OnInit { }, error: (error) => console.error("Error when retrieving capability version:" + error), }; - this.capabilityRequestService.getSpecificVersion(request_id, version_id).subscribe(specificVersionObserver); + this.capabilityRequestService + .getSpecificVersion(request_id, version_id) + .subscribe(specificVersionObserver); } - /** * Check if JsonObject is empty * Taken from: https://stackoverflow.com/questions/679915/how-do-i-test-for-an-empty-javascript-object @@ -85,5 +82,4 @@ export class VersionsComponent implements OnInit { return true; } - } diff --git a/shared/workspaces/test/test_capability_request.py b/shared/workspaces/test/test_capability_request.py new file mode 100644 index 0000000000000000000000000000000000000000..d09274c4d6e7fcf7175185389e821efe7cef9510 --- /dev/null +++ b/shared/workspaces/test/test_capability_request.py @@ -0,0 +1,225 @@ +# +# Copyright (C) 2021 Associated Universities, Inc. Washington DC, USA. +# +# This file is part of NRAO Workspaces. +# +# Workspaces is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Workspaces is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Workspaces. If not, see <https://www.gnu.org/licenses/>. + +from typing import Dict + +from hypothesis import given +from hypothesis import strategies as st + +from workspaces.capability.enums import CapabilityRequestState, CapabilityVersionState +from workspaces.capability.schema import CapabilityRequest, CapabilityVersion +from workspaces.capability.services.capability_info import CapabilityInfo + +from .conftest import ( + SAMPLE_CAPABILITY_NAMES, + SQLITE_MAX_INT, + SQLITE_MIN_INT, + clear_test_database, +) + +pytest_plugins = ["testing.utils.conftest"] + + +# Register CapabilityRequest JSON blueprint as a hypothesis type strategy +# To use: +# >>> @given(st.from_type(CapabilityRequest)) +# >>> def test(generated_request_json: Dict): +# >>> request = CapabilityRequest.from_json(generated_request_json) +st.register_type_strategy( + CapabilityRequest, + st.fixed_dictionaries( + { + "type": st.just("CapabilityRequest"), + "id": st.integers(min_value=SQLITE_MIN_INT, max_value=SQLITE_MAX_INT), + "capability_name": st.sampled_from(SAMPLE_CAPABILITY_NAMES), + "state": st.sampled_from([name for name, _ in CapabilityRequestState.__members__.items()]), + "parameters": st.one_of(st.lists(st.text()), st.none()), + "ingested": st.booleans(), + "created_at": st.datetimes().map( + lambda time: time.isoformat(), + ), + "updated_at": st.datetimes().map( + lambda time: time.isoformat(), + ), + "current_execution": st.none(), + } + ), +) + +# Register CapabilityVersion JSON blueprint as a hypothesis type strategy +# To use: +# >>> @given(st.from_type(CapabilityVersion)) +# >>> def test(generated_version_json: Dict): +# >>> version = CapabilityVersion.from_json(generated_version_json) +st.register_type_strategy( + CapabilityVersion, + st.fixed_dictionaries( + { + "type": st.just("CapabilityVersion"), + "capability_request_id": st.integers(min_value=SQLITE_MIN_INT, max_value=SQLITE_MAX_INT), + "version_number": st.integers(min_value=0, max_value=50), + "parameters": st.one_of(st.lists(st.text()), st.none()), + "workflow_metadata": st.one_of( + st.none(), st.dictionaries(st.text(min_size=1), st.text(min_size=1), min_size=1) + ), + "files": st.none(), + } + ), +) + + +@given( + st.from_type(CapabilityRequest), + st.lists( + st.from_type(CapabilityVersion), min_size=1, max_size=5, unique_by=lambda version: version["version_number"] + ), +) +def test_determine_state_complete(mock_capability_info: CapabilityInfo, request_json: Dict, list_of_version_json: Dict): + """ + Given: A capability request with multiple versions + When: The current version status is set to Complete + Then: The request state should be set to Complete + """ + request = CapabilityRequest.from_json(request_json) + versions = [CapabilityVersion.from_json(blob) for blob in list_of_version_json] + current_version_number = max(version.version_number for version in versions) + + with clear_test_database(mock_capability_info): + mock_capability_info.save_entity(request) + + for version in versions: + # Associate version with request + version.capability_request = request + version.capability_request_id = request.id + + # Set one version to Complete, the rest to Failed or Executing + if version.version_number == current_version_number: + version.state = CapabilityVersionState.Complete.name + else: + version.state = ( + CapabilityVersionState.Failed.name + if version.state == CapabilityVersionState.Complete.name or not version.state + else version.state + ) + + mock_capability_info.save_entity(version) + + request.determine_state() + assert request.state == CapabilityRequestState.Complete.name + + +@given( + st.from_type(CapabilityRequest), + st.lists( + st.from_type(CapabilityVersion), min_size=1, max_size=5, unique_by=lambda version: version["version_number"] + ), +) +def test_determine_state_submitted( + mock_capability_info: CapabilityInfo, request_json: Dict, list_of_version_json: Dict +): + """ + Given: A capability request with multiple versions + When: There are no versions in the Complete state + And: There is a mix of versions in either the Created, Submitted, or Failed states + Then: The request state is set to Executing + """ + request = CapabilityRequest.from_json(request_json) + versions = [CapabilityVersion.from_json(blob) for blob in list_of_version_json] + + with clear_test_database(mock_capability_info): + mock_capability_info.save_entity(request) + + for version in versions: + # Associate version with request + version.capability_request = request + version.capability_request_id = request.id + + # Get rid of Complete versions (set their state to Executing instead) + version.state = ( + CapabilityVersionState.Running.name + if version.state == CapabilityVersionState.Complete.name or not version.state + else version.state + ) + + mock_capability_info.save_entity(version) + + request.determine_state() + assert request.state == CapabilityRequestState.Submitted.name + + +@given( + st.from_type(CapabilityRequest), + st.lists( + st.from_type(CapabilityVersion), min_size=1, max_size=5, unique_by=lambda version: version["version_number"] + ), +) +def test_determine_state_failed(mock_capability_info: CapabilityInfo, request_json: Dict, list_of_version_json: Dict): + """ + Given: A capability request with multiple versions + When: All versions for the request are in the Failed state + Then: The request state is set to Failed + """ + request = CapabilityRequest.from_json(request_json) + versions = [CapabilityVersion.from_json(blob) for blob in list_of_version_json] + + with clear_test_database(mock_capability_info): + mock_capability_info.save_entity(request) + + for version in versions: + # Associate version with request + version.capability_request = request + version.capability_request_id = request.id + + # Set all versions to Failed state + version.state = CapabilityVersionState.Failed.name + + mock_capability_info.save_entity(version) + + request.determine_state() + assert request.state == CapabilityRequestState.Failed.name + + +@given( + st.from_type(CapabilityRequest), + st.lists( + st.from_type(CapabilityVersion), min_size=1, max_size=5, unique_by=lambda version: version["version_number"] + ), +) +def test_determine_state_created(mock_capability_info: CapabilityInfo, request_json: Dict, list_of_version_json: Dict): + """ + Given: A capability request with multiple versions + When: All versions for the request are in the Created state + Then: The request state is set to Created + """ + request = CapabilityRequest.from_json(request_json) + versions = [CapabilityVersion.from_json(blob) for blob in list_of_version_json] + + with clear_test_database(mock_capability_info): + mock_capability_info.save_entity(request) + for version in versions: + # Associate version with request + version.capability_request = request + version.capability_request_id = request.id + + # Set all versions to Failed state + version.state = CapabilityVersionState.Created.name + + mock_capability_info.save_entity(version) + + request.determine_state() + assert request.state == CapabilityRequestState.Created.name diff --git a/shared/workspaces/test/test_states.py b/shared/workspaces/test/test_states.py index 1a6515a6c23254fdf95197e6fcc0aece5b3cdf0c..b59af32f0d321caf058346975c578dfa4e26361f 100644 --- a/shared/workspaces/test/test_states.py +++ b/shared/workspaces/test/test_states.py @@ -23,7 +23,6 @@ The objective of this file is to help with prototyping and testing the capabilit from typing import Callable, Dict, List import pytest -import requests class Noop: diff --git a/shared/workspaces/workspaces/capability/schema.py b/shared/workspaces/workspaces/capability/schema.py index 90f9f9af237c5ca1df8753d8d60718c57fcf9c30..2cdafe8343c3da96afd236af9c2600eadbf0527c 100644 --- a/shared/workspaces/workspaces/capability/schema.py +++ b/shared/workspaces/workspaces/capability/schema.py @@ -25,7 +25,7 @@ import json import logging import pathlib import re -from typing import Dict, List +from typing import Dict, List, Optional import pendulum import requests @@ -239,15 +239,42 @@ class State(Base): transition.actions.append(action) self.transitions.append(transition) - def signal(self, message: Dict, execution: CapabilityExecutionIF, manager: ExecutionManagerIF) -> State: + def find_matching_transition(self, message: Dict) -> Optional[Transition]: + """ + Find matching transition for state based on a given message + + :param message: Message to match transition with + :return: Matching transition if one is found; else None + """ for transition in self.transitions: if transition.matches(message): - logger.info("Found matching transition") - transition.action(execution, manager) - return transition.to_state - logger.info("No matching transitions found") + return transition + return None + + def get_next_state(self, message: Dict) -> State: + """ + Get next state for current transition, given a message + + :param message: Message to match transition with + :return: Next state if matching transition is found; else current state + """ + transition = self.find_matching_transition(message) + if transition: + return transition.to_state return self + def perform_action(self, message: Dict, execution: CapabilityExecutionIF, manager: ExecutionManagerIF): + """ + Perform action for the current transition, if one exists + + :param message: Message to match transition with + :param execution: Execution that is performing the action + :param manager: Execution manager to send to the action + """ + transition = self.find_matching_transition(message) + if transition: + transition.action(execution, manager) + def final(self) -> bool: return not self.transitions @@ -440,6 +467,36 @@ class CapabilityRequest(Base, CapabilityRequestIF): def update_state(self, state: CapabilityRequestState): self.state = state.name + def determine_state(self): + """ + Determine state of request based on the state of its versions and set it accordingly + + RULES: + - If there is a complete version, the request is complete + - If all versions are failed, the request is failed + - If all versions are created, the request is created + - Otherwise, it is submitted + """ + logger.info("Determine request state...") + logger.info(f"Current version: {self.current_version.__json__()}") + version_states = [version.state for version in self.versions] + + if self.current_version.state == CapabilityVersionState.Complete.name: + # The current version is complete, so the request is complete + logger.info("Current version is complete. Setting request to Complete.") + self.state = CapabilityRequestState.Complete.name + elif all(state == CapabilityRequestState.Failed.name for state in version_states): + # Request has all failed versions, so it is failed + logger.info("All versions are failed. Setting request to Failed.") + self.state = CapabilityRequestState.Failed.name + elif all(state == CapabilityRequestState.Created.name for state in version_states): + # Request has no submitted versions, so it is still in the created state + logger.info("All versions are created. Setting request to Created.") + self.state = CapabilityRequestState.Created.name + else: + logger.info("Versions are in a mixture of states. Setting request to Submitted.") + self.state = CapabilityRequestState.Submitted.name + def __str__(self): return f"CapabilityRequest object: {self.__dict__}" @@ -684,4 +741,8 @@ class CapabilityExecution(Base, CapabilityExecutionIF, JSONSerializable): def signal(self, manager: ExecutionManagerIF, message: Dict): logger.info("Signalling my state %s with this message of type %s", self.state_name, message["type"]) - self.state = self.state.signal(message, self, manager) + previous_state = self.state + # Transition execution state + self.state = self.state.get_next_state(message) + # Perform action + previous_state.perform_action(message, self, manager) diff --git a/shared/workspaces/workspaces/capability/schema_interfaces.py b/shared/workspaces/workspaces/capability/schema_interfaces.py index 787f037fdc3411870c715102226a6e1aecd9607e..ffd40012d3d5f34d99228c630aaafa14f0530266 100644 --- a/shared/workspaces/workspaces/capability/schema_interfaces.py +++ b/shared/workspaces/workspaces/capability/schema_interfaces.py @@ -15,13 +15,14 @@ # # You should have received a copy of the GNU General Public License # along with Workspaces. If not, see <https://www.gnu.org/licenses/>. + +# pylint: disable=C0114, C0115, C0116, R0903 + from __future__ import annotations import pathlib from typing import Dict, List -# pylint: disable=C0114, C0115, C0116, R0903 - from workspaces.capability.helpers_interfaces import ParameterIF from workspaces.products.schema_interfaces import FutureProductIF from workspaces.system.schema import JSONSerializable @@ -48,6 +49,9 @@ class CapabilityRequestIF(JSONSerializable): def current_version(self): raise NotImplementedError + def determine_state(self): + pass + class CapabilityVersionIF: capability_request: CapabilityRequestIF diff --git a/shared/workspaces/workspaces/capability/services/capability_info.py b/shared/workspaces/workspaces/capability/services/capability_info.py index e73b061e5c5a7eb0a1b36b449150721c3e98ec93..2aea8a830aeb83768a24296bed0ae1164cbbfce7 100644 --- a/shared/workspaces/workspaces/capability/services/capability_info.py +++ b/shared/workspaces/workspaces/capability/services/capability_info.py @@ -178,8 +178,9 @@ class CapabilityInfo(CapabilityInfoIF): :return: new CapabilityVersion """ request = self.lookup_capability_request(capability_request_id) - # Reset request state to Created - request.state = CapabilityRequestState.Created.name + # Reset request state accordingly + + request.determine_state() self.save_entity(request) logger.info(f"Parent Request: {request.__json__()}") diff --git a/shared/workspaces/workspaces/capability/services/capability_service.py b/shared/workspaces/workspaces/capability/services/capability_service.py index 5b24a69a021adafdfcce9936d5687bdbb7db21d8..b990b3b1c3f5a987b610421cb01ea6f5126e5d9b 100644 --- a/shared/workspaces/workspaces/capability/services/capability_service.py +++ b/shared/workspaces/workspaces/capability/services/capability_service.py @@ -22,7 +22,7 @@ import transaction from messaging.messenger import MessageSender from messaging.router import Router, on_message -from workspaces.capability.enums import CapabilityRequestState, CapabilityVersionState +from workspaces.capability.enums import CapabilityVersionState from workspaces.capability.helpers import Parameter from workspaces.capability.message_architect import CapabilityMessageArchitect from workspaces.capability.schema import CapabilityRequest @@ -70,17 +70,18 @@ class CapabilityService(CapabilityServiceIF): def complete_request(self, **message: Dict): logger.info(f"RECEIVED EXECUTION-COMPLETE: {message}") - # Set request state to Complete execution = message["subject"] capability_request = self.capability_info.lookup_capability_request(execution["capability_request_id"]) - capability_request.state = CapabilityRequestState.Complete.name - self.capability_info.save_entity(capability_request) - - # Set version state to Complete version = self.capability_info.lookup_version(capability_request.id, execution["version_number"]) - version.state = CapabilityVersionState.Complete.name + + # Set version state to Complete if the execution is indeed complete; else, leave it alone + version.state = CapabilityVersionState.Complete.name if execution["state_name"] == "Complete" else version.state self.capability_info.save_entity(version) + # Update request state + capability_request.determine_state() + self.capability_info.save_entity(capability_request) + capability_complete_msg = CapabilityMessageArchitect(request=capability_request).compose_message( "capability_complete" ) @@ -91,18 +92,17 @@ class CapabilityService(CapabilityServiceIF): logger.info(f"RECEIVED EXECUTION-FAILED: {message}") execution = message["subject"] - - # Set request state to Failed - # TODO(nhertz): Dynamically calculate request state based on the states of its versions capability_request = self.capability_info.lookup_capability_request(execution["capability_request_id"]) - capability_request.state = CapabilityRequestState.Failed.name - self.capability_info.save_entity(capability_request) + version = self.capability_info.lookup_version(capability_request.id, execution["version_number"]) # Set version state to Failed - version = self.capability_info.lookup_version(capability_request.id, execution["version_number"]) version.state = CapabilityVersionState.Failed.name self.capability_info.save_entity(version) + # Update request state + capability_request.determine_state() + self.capability_info.save_entity(capability_request) + capability_failed_msg = CapabilityMessageArchitect(request=capability_request).compose_message( "capability_failed" )