merged in main and resolved conflicts w/ burnettk

This commit is contained in:
jasquat 2023-03-06 11:15:16 -05:00
commit 9a1a8a4fbe
No known key found for this signature in database
20 changed files with 4400 additions and 4236 deletions

View File

@ -3,7 +3,7 @@ import {
BpmnPropertiesPanelModule,
BpmnPropertiesProviderModule,
} from 'bpmn-js-properties-panel';
import diagramXML from '../test/spec/bpmn/basic_message.bpmn';
import diagramXML from '../test/spec/bpmn/empty_diagram.bpmn';
import spiffworkflow from './spiffworkflow';
import setupFileOperations from './fileOperations';

View File

@ -4,9 +4,9 @@
* @param container
*/
export function findDataObjects(parent) {
let dataObjects = [];
export function findDataObjects(parent, dataObjects) {
if (typeof(dataObjects) === 'undefined')
dataObjects = [];
let process;
if (!parent) {
return [];
@ -15,16 +15,13 @@ export function findDataObjects(parent) {
process = parent.processRef;
} else {
process = parent;
if (process.$type === 'bpmn:SubProcess')
findDataObjects(process.$parent, dataObjects);
}
if (!process.flowElements) {
return [];
}
for (const element of process.flowElements) {
if (
element.$type === 'bpmn:DataObject' &&
dataObjects.indexOf(element) < 0
) {
dataObjects.push(element);
if (typeof(process.flowElements) !== 'undefined') {
for (const element of process.flowElements) {
if (element.$type === 'bpmn:DataObject')
dataObjects.push(element);
}
}
return dataObjects;
@ -38,16 +35,26 @@ export function findDataObject(process, id) {
}
}
export function findDataReferenceShapes(processShape, id) {
let refs = [];
for (const shape of processShape.children) {
if (shape.type === 'bpmn:DataObjectReference') {
if (shape.businessObject.dataObjectRef && shape.businessObject.dataObjectRef.id === id) {
refs.push(shape);
}
}
}
return refs;
export function findDataObjectReferences(children, dataObjectId) {
return children.flatMap((child) => {
if (child.$type == 'bpmn:DataObjectReference' && child.dataObjectRef.id == dataObjectId)
return [child];
else if (child.$type == 'bpmn:SubProcess')
return findDataObjectReferences(child.get('flowElements'), dataObjectId);
else
return [];
});
}
export function findDataObjectReferenceShapes(children, dataObjectId) {
return children.flatMap((child) => {
if (child.type == 'bpmn:DataObjectReference' && child.businessObject.dataObjectRef.id == dataObjectId)
return [child];
else if (child.type == 'bpmn:SubProcess')
return findDataObjectReferenceShapes(child.children, dataObjectId);
else
return [];
});
}
export function idToHumanReadableName(id) {

View File

@ -3,11 +3,12 @@ import { getDi, is } from 'bpmn-js/lib/util/ModelUtil';
import { remove as collectionRemove } from 'diagram-js/lib/util/Collections';
import {
findDataObjects,
findDataReferenceShapes,
findDataObjectReferences,
idToHumanReadableName,
} from './DataObjectHelpers';
const HIGH_PRIORITY = 1500;
/**
* This Command Interceptor functions like the BpmnUpdator in BPMN.js - It hooks into events
* from Diagram.js and updates the underlying BPMN model accordingly.
@ -20,9 +21,40 @@ const HIGH_PRIORITY = 1500;
* 4) Don't allow someone to move a DataObjectReference from one process to another process.
*/
export default class DataObjectInterceptor extends CommandInterceptor {
constructor(eventBus, bpmnFactory, commandStack) {
constructor(eventBus, bpmnFactory, commandStack, bpmnUpdater) {
super(eventBus);
/* The default behavior is to move the data object into whatever object the reference is being created in.
* If a data object already has a parent, don't change it.
*/
bpmnUpdater.updateSemanticParent = (businessObject, parentBusinessObject) => {
// Special case for participant - which is a valid place to drop a data object, but it needs to be added
// to the particpant's Process (which isn't directly accessible in BPMN.io
let realParent = parentBusinessObject;
if (is(realParent, 'bpmn:Participant')) {
realParent = realParent.processRef;
}
if (is(businessObject, 'bpmn:DataObjectReference')) {
// For data object references, always update the flowElements when a parent is provided
// The parent could be null if it's being deleted, and I could probably handle that here instead of
// when the shape is deleted, but not interested in refactoring at the moment.
if (realParent != null) {
const flowElements = realParent.get('flowElements');
flowElements.push(businessObject);
}
} else if (is(businessObject, 'bpmn:DataObject')) {
// For data objects, only update the flowElements for new data objects, and set the parent so it doesn't get moved.
if (typeof(businessObject.$parent) === 'undefined') {
const flowElements = realParent.get('flowElements');
flowElements.push(businessObject);
businessObject.$parent = realParent;
}
} else
bpmnUpdater.__proto__.updateSemanticParent.call(this, businessObject, parentBusinessObject);
};
/**
* For DataObjectReferences only ...
* Prevent this from calling the CreateDataObjectBehavior in BPMN-js, as it will
@ -52,9 +84,9 @@ export default class DataObjectInterceptor extends CommandInterceptor {
} else {
dataObject = bpmnFactory.create('bpmn:DataObject');
}
// set the reference to the DataObject
shape.businessObject.dataObjectRef = dataObject;
shape.businessObject.$parent = process;
}
});
@ -84,38 +116,19 @@ export default class DataObjectInterceptor extends CommandInterceptor {
*/
this.executed(['shape.delete'], HIGH_PRIORITY, function (event) {
const { context } = event;
const { shape, oldParent } = context;
const { shape } = context;
if (is(shape, 'bpmn:DataObjectReference') && shape.type !== 'label') {
const references = findDataReferenceShapes(
oldParent,
shape.businessObject.dataObjectRef.id
);
const dataObject = shape.businessObject.dataObjectRef;
let flowElements = shape.businessObject.$parent.get('flowElements');
collectionRemove(flowElements, shape.businessObject);
let references = findDataObjectReferences(flowElements, dataObject.id);
if (references.length === 0) {
return; // Use the default bahavior and delete the data object.
let flowElements = dataObject.$parent.get('flowElements');
collectionRemove(flowElements, dataObject);
}
// Remove the business Object
let containment = '';
const { businessObject } = shape;
if (is(businessObject, 'bpmn:DataOutputAssociation')) {
containment = 'dataOutputAssociations';
}
if (is(businessObject, 'bpmn:DataInputAssociation')) {
containment = 'dataInputAssociations';
}
const children = businessObject.$parent.get(containment);
collectionRemove(children, businessObject);
// Remove the visible element.
const di = getDi(shape);
const planeElements = di.$parent.get('planeElement');
collectionRemove(planeElements, di);
di.$parent = null;
// Stop the propogation.
event.stopPropagation();
}
});
}
}
DataObjectInterceptor.$inject = ['eventBus', 'bpmnFactory', 'commandStack'];
DataObjectInterceptor.$inject = ['eventBus', 'bpmnFactory', 'commandStack', 'bpmnUpdater'];

View File

@ -5,7 +5,11 @@ import {
} from '@bpmn-io/properties-panel';
import { without } from 'min-dash';
import { is } from 'bpmn-js/lib/util/ModelUtil';
import {findDataObjects, findDataReferenceShapes, idToHumanReadableName} from '../DataObjectHelpers';
import {
findDataObjects,
findDataObjectReferenceShapes,
idToHumanReadableName,
} from '../DataObjectHelpers';
/**
* Provides a list of data objects, and allows you to add / remove data objects, and change their ids.
@ -20,7 +24,7 @@ export function DataObjectArray(props) {
let process;
// This element might be a process, or something that will reference a process.
if (is(element.businessObject, 'bpmn:Process')) {
if (is(element.businessObject, 'bpmn:Process') || is(element.businessObject, 'bpmn:SubProcess')) {
process = element.businessObject;
} else if (element.businessObject.processRef) {
process = element.businessObject.processRef;
@ -53,6 +57,7 @@ export function DataObjectArray(props) {
const newDataObject = moddle.create('bpmn:DataObject');
const newElements = process.get('flowElements');
newDataObject.id = moddle.ids.nextPrefixed('DataObject_');
newDataObject.$parent = process;
newElements.push(newDataObject);
commandStack.execute('element.updateModdleProperties', {
element,
@ -79,7 +84,7 @@ function removeFactory(props) {
},
});
// When a data object is removed, remove all references as well.
const references = findDataReferenceShapes(element, dataObject.id);
const references = findDataObjectReferenceShapes(element.children, dataObject.id);
for (const ref of references) {
commandStack.execute('shape.delete', { shape: ref });
}
@ -116,7 +121,7 @@ function DataObjectTextField(props) {
});
// Also update the label of all the references
const references = findDataReferenceShapes(element, dataObject.id);
const references = findDataObjectReferenceShapes(element.children, dataObject.id);
for (const ref of references) {
commandStack.execute('element.updateProperties', {
element: ref,

View File

@ -20,7 +20,8 @@ export default function DataObjectPropertiesProvider(
);
}
if (
isAny(element, ['bpmn:Process', 'bpmn:SubProcess', 'bpmn:Participant'])
isAny(element, ['bpmn:Process', 'bpmn:Participant']) ||
(is(element, 'bpmn:SubProcess') && !element.collapsed)
) {
groups.push(
createDataObjectEditor(

View File

@ -4,7 +4,11 @@ import { BpmnPropertiesPanelModule, BpmnPropertiesProviderModule } from 'bpmn-js
import {
inject,
} from 'bpmn-js/test/helper';
import {findDataObjects, idToHumanReadableName} from '../../app/spiffworkflow/DataObject/DataObjectHelpers';
import {
findDataObjects,
findDataObjectReferenceShapes,
idToHumanReadableName,
} from '../../app/spiffworkflow/DataObject/DataObjectHelpers';
describe('DataObject Interceptor', function() {
@ -113,4 +117,52 @@ describe('DataObject Interceptor', function() {
expect(dataObjects.length).to.equal(1);
}));
it('Data objects in a process should be visible in a subprocess', inject(function(canvas, modeling, elementRegistry) {
let subProcessShape = elementRegistry.get('my_subprocess');
let subProcess = subProcessShape.businessObject;
let dataObjects = findDataObjects(subProcess);
expect(dataObjects.length).to.equal(0);
let rootShape = canvas.getRootElement();
const dataObjectRefShape = modeling.createShape({ type: 'bpmn:DataObjectReference' },
{ x: 220, y: 220 }, rootShape);
dataObjects = findDataObjects(subProcess);
expect(dataObjects.length).to.equal(1);
}));
it('Data objects in a subprocess should not be visible in a process', inject(function(canvas, modeling, elementRegistry) {
let subProcessShape = elementRegistry.get('my_subprocess');
let subProcess = subProcessShape.businessObject;
const dataObjectRefShape = modeling.createShape({ type: 'bpmn:DataObjectReference' },
{ x: 220, y: 220 }, subProcessShape);
let dataObjects = findDataObjects(subProcess);
expect(dataObjects.length).to.equal(1);
let rootShape = canvas.getRootElement();
dataObjects = findDataObjects(rootShape);
expect(dataObjects.length).to.equal(0);
}));
it('References inside subprocesses should be visible in a process', inject(function(canvas, modeling, elementRegistry) {
let rootShape = canvas.getRootElement();
const refOne = modeling.createShape({ type: 'bpmn:DataObjectReference' },
{ x: 220, y: 220 }, rootShape);
let subProcessShape = elementRegistry.get('my_subprocess');
let subProcess = subProcessShape.businessObject;
const refTwo = modeling.createShape({ type: 'bpmn:DataObjectReference' },
{ x: 320, y: 220 }, subProcessShape);
let dataObjects = findDataObjects(subProcess);
expect(dataObjects.length).to.equal(1);
let references = findDataObjectReferenceShapes(rootShape.children, dataObjects[0].id);
expect(references.length).to.equal(2);
}));
});

4037
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
[tool.poetry]
name = "spiffworkflow-arean"
name = "spiffworkflow-arena"
version = "0.0.0"
description = "Spiffworkflow Arena"
authors = ["Jason Lantz <sartography@users.noreply.github.com>"]
license = "MIT"
readme = "README.rst"
readme = "README.md"
homepage = "https://github.com/sartography/spiffworkflow-arena"
repository = "https://github.com/sartography/spiffworkflow-arena"
classifiers = [
@ -48,7 +48,6 @@ APScheduler = "^3.9.1"
Jinja2 = "^3.1.2"
RestrictedPython = "^6.0"
Flask-SQLAlchemy = "^3"
orjson = "^3.8.0"
# type hinting stuff
# these need to be in the normal (non dev-dependencies) section

View File

@ -1,16 +1,16 @@
"""empty message
Revision ID: 55b76c4528c5
Revision ID: ede2ae7d3c80
Revises:
Create Date: 2023-03-06 11:11:55.431564
Create Date: 2023-03-06 11:14:40.739641
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '55b76c4528c5'
revision = 'ede2ae7d3c80'
down_revision = None
branch_labels = None
depends_on = None
@ -303,7 +303,7 @@ def upgrade():
sa.Column('list_index', sa.Integer(), nullable=True),
sa.Column('mimetype', sa.String(length=255), nullable=False),
sa.Column('filename', sa.String(length=255), nullable=False),
sa.Column('contents', sa.LargeBinary(), nullable=False),
sa.Column('contents', sa.LargeBinary().with_variant(mysql.LONGBLOB(), 'mysql'), nullable=False),
sa.Column('digest', sa.String(length=64), nullable=False),
sa.Column('updated_at_in_seconds', sa.Integer(), nullable=False),
sa.Column('created_at_in_seconds', sa.Integer(), nullable=False),

File diff suppressed because it is too large Load Diff

View File

@ -75,7 +75,7 @@ pylint = "^2.15.10"
[tool.poetry.dev-dependencies]
pytest = "*"
pytest = "^7.1.2"
coverage = {extras = ["toml"], version = "^6.1"}
safety = "^2.3.1"
mypy = ">=0.961"
@ -84,12 +84,12 @@ xdoctest = {extras = ["colors"], version = "^1.0.1"}
sphinx = "^5.0.2"
sphinx-autobuild = ">=2021.3.14"
pre-commit = "^2.20.0"
flake8 = "*"
flake8 = "^4.0.1"
black = ">=21.10b0"
flake8-bandit = "*"
flake8-bandit = "^2.1.2"
# 1.7.3 broke us. https://github.com/PyCQA/bandit/issues/841
bandit = "*"
bandit = "1.7.2"
flake8-bugbear = "^22.10.25"
flake8-docstrings = "^1.6.0"

View File

@ -3,6 +3,7 @@ from dataclasses import dataclass
from typing import Optional
from sqlalchemy import ForeignKey
from sqlalchemy.dialects.mysql import LONGBLOB
from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
@ -24,7 +25,9 @@ class ProcessInstanceFileDataModel(SpiffworkflowBaseDBModel):
mimetype: str = db.Column(db.String(255), nullable=False)
filename: str = db.Column(db.String(255), nullable=False)
# this is not deferred because there is no reason to query this model if you do not want the contents
contents: str = db.Column(db.LargeBinary, nullable=False)
contents: str = db.Column(
db.LargeBinary().with_variant(LONGBLOB, "mysql"), nullable=False
)
digest: str = db.Column(db.String(64), nullable=False, index=True)
updated_at_in_seconds: int = db.Column(db.Integer, nullable=False)

View File

@ -14,7 +14,6 @@ from sqlalchemy import ForeignKey
from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel
from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
from spiffworkflow_backend.models.task_definition import TaskDefinitionModel
class MultiInstanceType(enum.Enum):

View File

@ -1,7 +1,7 @@
"""Process_instance_processor."""
import _strptime # type: ignore
from SpiffWorkflow.task import TaskStateNames # type: ignore
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
from SpiffWorkflow.task import TaskStateNames # type: ignore
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
import decimal
import json
import logging
@ -987,7 +987,8 @@ class ProcessInstanceProcessor:
bpmn_process = None
if bpmn_process_parent is not None:
bpmn_process = BpmnProcessModel.query.filter_by(parent_process_id=bpmn_process_parent.id, guid=bpmn_process_guid).first()
bpmn_process = BpmnProcessModel.query.filter_by(
parent_process_id=bpmn_process_parent.id, guid=bpmn_process_guid).first()
elif self.process_instance_model.bpmn_process_id is not None:
bpmn_process = self.process_instance_model.bpmn_process

View File

@ -24,13 +24,19 @@ const deleteVideosOnSuccess = (on) => {
})
}
let spiffWorkflowFrontendUrl = `http://localhost:${process.env.SPIFFWORKFLOW_FRONTEND_PORT || 7001}`
if (process.env.SPIFFWORKFLOW_FRONTEND_URL) {
spiffWorkflowFrontendUrl = process.env.SPIFFWORKFLOW_FRONTEND_URL
}
const cypressConfig = {
projectId: 'crax1q',
videoUploadOnPasses: false,
chromeWebSecurity: false,
e2e: {
baseUrl: `http://localhost:${process.env.SPIFFWORKFLOW_FRONTEND_PORT || 7001}`,
baseUrl: spiffWorkflowFrontendUrl,
setupNodeEvents(on, config) {
deleteVideosOnSuccess(on)
require('@cypress/grep/src/plugin')(config);

View File

@ -0,0 +1,104 @@
const approveWithUser = (
username,
processInstanceId,
expectAdditionalApprovalInfoPage = false
) => {
cy.login(username, username);
cy.visit('/admin/process-instances/find-by-id');
cy.get('#process-instance-id-input').type(processInstanceId);
cy.get('button')
.contains(/^Submit$/)
.click();
cy.contains('Tasks I can complete', { timeout: 20000 });
cy.get('.cds--btn').contains(/^Go$/).click();
// approve!
cy.get('#root-app').click();
cy.get('button')
.contains(/^Submit$/)
.click();
if (expectAdditionalApprovalInfoPage) {
cy.contains(expectAdditionalApprovalInfoPage, { timeout: 20000 });
cy.get('button')
.contains(/^Continue$/)
.click();
}
cy.location({ timeout: 20000 }).should((loc) => {
expect(loc.pathname).to.eq('/tasks');
});
cy.logout();
};
describe('pp1', () => {
it('can run PP1', () => {
cy.login('core5.contributor', 'core5.contributor');
cy.visit('/');
cy.contains('Start New +').click();
cy.contains('Raise New Demand Request');
cy.runPrimaryBpmnFile(true);
cy.contains('Procurement').click();
// cy.contains('Submit').click();
cy.get('button')
.contains(/^Submit$/)
.click();
cy.contains(
'Submit a new demand request for the procurement of needed items',
{ timeout: 20000 }
);
cy.url().then((currentUrl) => {
// if url is "/tasks/8/d37c2f0f-016a-4066-b669-e0925b759560"
// extract the digits after /tasks
const processInstanceId = currentUrl.match(/(?<=\/tasks\/)\d+/)[0];
cy.get('#root_project').select('18564');
cy.get('#root_category').select('soft_and_lic');
cy.get('#root_purpose').clear().type('need the software for my work');
cy.get('#root_criticality').select('High');
cy.get('#root_period').clear().type('2023-10-10');
cy.get('#root_vendor').clear().type('sartography');
cy.get('#root_payment_method').select('Bank Transfer');
cy.get('#root_project').select('18564');
cy.get('#root_category').select('soft_and_lic');
cy.get('button')
.contains(/^Submit$/)
.click();
cy.contains('Task: Enter NDR Items', { timeout: 20000 });
cy.get('#root_0_sub_category').select('op_src');
cy.get('#root_0_item').clear().type('spiffworkflow');
cy.get('#root_0_qty').clear().type('1');
cy.get('#root_0_currency_type').select('Fiat');
cy.get('#root_0_currency').select('AUD');
cy.get('#root_0_unit_price').type('100');
cy.get('button')
.contains(/^Submit$/)
.click();
cy.contains(
'Review and provide any supporting information or files for your request.'
);
cy.contains('Submit the Request').click();
cy.get('input[value="Submit the Request"]').click();
cy.get('button')
.contains(/^Submit$/)
.click();
cy.logout();
approveWithUser(
'infra.project-lead',
processInstanceId,
'Task: Reminder: Request Additional Budget'
);
approveWithUser('ppg.ba.sme', processInstanceId);
approveWithUser('security.sme', processInstanceId);
approveWithUser(
'infra.sme',
processInstanceId,
'Task: Update Application Landscape'
);
approveWithUser('legal.sme', processInstanceId);
});
});
});

View File

@ -41,10 +41,15 @@ Cypress.Commands.add('navigateToAdmin', () => {
cy.visit('/admin');
});
Cypress.Commands.add('login', (selector, ...args) => {
Cypress.Commands.add('login', (username, password) => {
// Cypress.Commands.add('login', (selector, ...args) => {
cy.visit('/admin');
const username = Cypress.env('SPIFFWORKFLOW_FRONTEND_USERNAME') || 'ciadmin1';
const password = Cypress.env('SPIFFWORKFLOW_FRONTEND_PASSWORD') || 'ciadmin1';
if (!username) {
const username =
Cypress.env('SPIFFWORKFLOW_FRONTEND_USERNAME') || 'ciadmin1';
const password =
Cypress.env('SPIFFWORKFLOW_FRONTEND_PASSWORD') || 'ciadmin1';
}
cy.get('#username').type(username);
cy.get('#password').type(password);
if (Cypress.env('SPIFFWORKFLOW_FRONTEND_AUTH_WITH_KEYCLOAK') === true) {
@ -97,7 +102,12 @@ Cypress.Commands.add('createModel', (groupId, modelId, modelDisplayName) => {
Cypress.Commands.add(
'runPrimaryBpmnFile',
(expectAutoRedirectToHumanTask = false) => {
cy.contains('Start').click();
// cy.getBySel('start-process-instance').click();
// click on button with text Start
cy.get('button')
.contains(/^Start$/)
.click();
if (expectAutoRedirectToHumanTask) {
// the url changes immediately, so also make sure we get some content from the next page, "Task:", or else when we try to interact with the page, it'll re-render and we'll get an error with cypress.
cy.url().should('include', `/tasks/`);

View File

@ -8065,7 +8065,7 @@
},
"node_modules/bpmn-js-spiffworkflow": {
"version": "0.0.8",
"resolved": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#f1f008e3e39be43b016718fca6a38b248ab4ecf7",
"resolved": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#82260144f90d9a311155066d637664d9e2a3f02e",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.4",
@ -38214,7 +38214,7 @@
}
},
"bpmn-js-spiffworkflow": {
"version": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#f1f008e3e39be43b016718fca6a38b248ab4ecf7",
"version": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#82260144f90d9a311155066d637664d9e2a3f02e",
"from": "bpmn-js-spiffworkflow@sartography/bpmn-js-spiffworkflow#main",
"requires": {
"inherits": "^2.0.4",

View File

@ -99,3 +99,6 @@ a:link {
padding-bottom: 70px;
min-height: 100%;
}
.djs-palette.two-column.open {
width: 96px !important;
}

View File

@ -126,7 +126,11 @@ export default function ProcessInstanceRun({
if (checkPermissions) {
return (
<Can I="POST" a={processInstanceCreatePath} ability={ability}>
<Button onClick={processInstanceCreateAndRun} className={className}>
<Button
data-qa="start-process-instance"
onClick={processInstanceCreateAndRun}
className={className}
>
Start
</Button>
</Can>