Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""
Add PipelineActionCondition and ActionInstanceCondition tables

Revision ID: d5c1f4b7a9e0
Revises: cf1a4a9becad
Create Date: 2025-09-24 09:58:00

"""

# revision identifiers, used by Alembic.
revision = 'd5c1f4b7a9e0'
down_revision = 'cf1a4a9becad'
branch_labels = None
depends_on = None

from alembic import op
import sqlalchemy as sa


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
'pipeline_action_conditions',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('pipeline_id', sa.Integer(), nullable=False),
sa.Column('action_id', sa.Integer(), nullable=False),
sa.Column('status_id', sa.Integer(), nullable=False),
sa.Column('expression', sa.Text(), nullable=False),
sa.ForeignKeyConstraint(['action_id'], ['actions.id'], ),
sa.ForeignKeyConstraint(['pipeline_id'], ['pipelines.id'], ),
sa.ForeignKeyConstraint(['status_id'], ['statuses.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('pipeline_id', 'action_id', name='uq_pipeline_action_condition'),
mysql_engine='InnoDB'
)
op.create_index(op.f('ix_pipeline_action_conditions_action_id'), 'pipeline_action_conditions', ['action_id'], unique=False)
op.create_index(op.f('ix_pipeline_action_conditions_id'), 'pipeline_action_conditions', ['id'], unique=False)
op.create_index(op.f('ix_pipeline_action_conditions_pipeline_id'), 'pipeline_action_conditions', ['pipeline_id'], unique=False)
op.create_index(op.f('ix_pipeline_action_conditions_status_id'), 'pipeline_action_conditions', ['status_id'], unique=False)

op.create_table(
'action_instance_conditions',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('action_instance_id', sa.Integer(), nullable=False),
sa.Column('pipeline_id', sa.Integer(), nullable=False),
sa.Column('action_id', sa.Integer(), nullable=False),
sa.Column('status_id', sa.Integer(), nullable=False),
sa.Column('expression', sa.Text(), nullable=False),
sa.ForeignKeyConstraint(['action_id'], ['actions.id'], ),
sa.ForeignKeyConstraint(['action_instance_id'], ['action_instances.id'], ),
sa.ForeignKeyConstraint(['pipeline_id'], ['pipelines.id'], ),
sa.ForeignKeyConstraint(['status_id'], ['statuses.id'], ),
sa.PrimaryKeyConstraint('id'),
mysql_engine='InnoDB'
)
op.create_index(op.f('ix_action_instance_conditions_action_id'), 'action_instance_conditions', ['action_id'], unique=False)
op.create_index(op.f('ix_action_instance_conditions_action_instance_id'), 'action_instance_conditions', ['action_instance_id'], unique=False)
op.create_index(op.f('ix_action_instance_conditions_id'), 'action_instance_conditions', ['id'], unique=False)
op.create_index(op.f('ix_action_instance_conditions_pipeline_id'), 'action_instance_conditions', ['pipeline_id'], unique=False)
op.create_index(op.f('ix_action_instance_conditions_status_id'), 'action_instance_conditions', ['status_id'], unique=False)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_action_instance_conditions_status_id'), table_name='action_instance_conditions')
op.drop_index(op.f('ix_action_instance_conditions_pipeline_id'), table_name='action_instance_conditions')
op.drop_index(op.f('ix_action_instance_conditions_id'), table_name='action_instance_conditions')
op.drop_index(op.f('ix_action_instance_conditions_action_instance_id'), table_name='action_instance_conditions')
op.drop_index(op.f('ix_action_instance_conditions_action_id'), table_name='action_instance_conditions')
op.drop_table('action_instance_conditions')

op.drop_index(op.f('ix_pipeline_action_conditions_status_id'), table_name='pipeline_action_conditions')
op.drop_index(op.f('ix_pipeline_action_conditions_pipeline_id'), table_name='pipeline_action_conditions')
op.drop_index(op.f('ix_pipeline_action_conditions_id'), table_name='pipeline_action_conditions')
op.drop_index(op.f('ix_pipeline_action_conditions_action_id'), table_name='pipeline_action_conditions')
op.drop_table('pipeline_action_conditions')
# ### end Alembic commands ###
27 changes: 26 additions & 1 deletion rapid/workflow/data/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import datetime

from sqlalchemy.orm import relationship
from sqlalchemy import Column, String, ForeignKey, Integer, Boolean, Text
from sqlalchemy import Column, String, ForeignKey, Integer, Boolean, Text, UniqueConstraint

from rapid.lib import get_declarative_base
from rapid.lib.constants import StatusConstants
Expand Down Expand Up @@ -230,4 +230,29 @@ class PipelineEvent(BaseModel, Base):
conditional = Column(Text, nullable=False)
config = Column(Text, nullable=False)

class PipelineActionCondition(BaseModel, Base):
pipeline_id = Column(Integer, ForeignKey('pipelines.id'), nullable=False, index=True)
action_id = Column(Integer, ForeignKey('actions.id'), nullable=False, index=True)
status_id = Column(Integer, ForeignKey('statuses.id'), nullable=False, index=True)
expression = Column(Text, nullable=False)

pipeline = relationship('Pipeline')
action = relationship('Action')
status = relationship('Status')

__table_args__ = (
UniqueConstraint('pipeline_id', 'action_id', name='uq_pipeline_action_condition'),
)


class ActionInstanceCondition(BaseModel, Base):
action_instance_id = Column(Integer, ForeignKey('action_instances.id'), nullable=False, index=True)
pipeline_id = Column(Integer, ForeignKey('pipelines.id'), nullable=False, index=True)
action_id = Column(Integer, ForeignKey('actions.id'), nullable=False, index=True)
status_id = Column(Integer, ForeignKey('statuses.id'), nullable=False, index=True)
expression = Column(Text, nullable=False)

action_instance = relationship('ActionInstance')
pipeline = relationship('Pipeline')
action = relationship('Action')
status = relationship('Status')
133 changes: 133 additions & 0 deletions tests/workflow/test_conditions_endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import json
from unittest.mock import MagicMock, patch

from rapid.workflow.api_controller import APIRouter
from tests.framework.unit_test import UnitTest


class TestPipelineActionConditionsEndpoints(UnitTest):
def setUp(self):
self.controller = self.fill_object_with_mocks(APIRouter)
self.request = MagicMock()
self.controller.http_wrapper.current_request.return_value = self.request

self.fake_session = MagicMock()
self.get_db_session_patcher = patch('rapid.workflow.api_controller.get_db_session', return_value=[self.fake_session])
self.get_db_session_patcher.start()
self.addCleanup(self.get_db_session_patcher.stop)
self.controller._load_objects()
self.dal = MagicMock()

def test_endpoints_are_discoverable(self):
assert self.controller._is_valid('pipeline_action_conditions')
assert self.controller._is_valid('action_instance_conditions')

def test_metadata_available_for_new_models(self):
resp = self.controller.metadata('pipeline_action_conditions')
data = json.loads(resp.get_data(as_text=True))

assert 'pipeline_id' in data
assert 'action_id' in data
assert 'status_id' in data
assert 'expression' in data

resp = self.controller.metadata('action_instance_conditions')

data = json.loads(resp.get_data(as_text=True))
assert 'action_instance_id' in data
assert 'pipeline_id' in data
assert 'action_id' in data
assert 'status_id' in data
assert 'expression' in data

@patch.object(APIRouter, APIRouter._retrieve_dal.__name__)
def test_pipeline_action_conditions_create(self, mock_retrieve_dal):
body = {"pipeline_id": 1, "action_id": 2, "status_id": 9, "expression": "x == 1"}
self.request.get_json.return_value = body

self.dal.create_object.return_value = {"id": 123, **body}
mock_retrieve_dal.return_value = self.dal

resp = self.controller.create('pipeline_action_conditions')

data = json.loads(resp.get_data(as_text=True))
assert data['id'] == 123
assert data['pipeline_id'] == 1
self.dal.create_object.assert_called()

@patch.object(APIRouter, APIRouter._retrieve_dal.__name__)
def test_pipeline_action_conditions_edit(self, mock_retrieve_dal):
self.request.json = {"expression": "x == 2"}

instance = MagicMock()
instance.serialize.return_value = {"id": 123, "expression": "x == 2"}
self.dal.edit_object.return_value = instance
mock_retrieve_dal.return_value = self.dal

resp = self.controller.edit_object('pipeline_action_conditions', 123)

data = json.loads(resp.get_data(as_text=True))
assert data['id'] == 123
assert data['expression'] == 'x == 2'
self.dal.edit_object.assert_called()

@patch.object(APIRouter, APIRouter._retrieve_dal.__name__)
def test_pipeline_action_conditions_delete(self, mock_retrieve_dal):
instance = MagicMock()
instance.serialize.return_value = {"id": 123}
self.dal.delete_object.return_value = instance
mock_retrieve_dal.return_value = self.dal

resp = self.controller.delete_object('pipeline_action_conditions', 123)

data = json.loads(resp.get_data(as_text=True))
assert data['id'] == 123
self.dal.delete_object.assert_called()

@patch.object(APIRouter, APIRouter._retrieve_dal.__name__)
def test_action_instance_conditions_create(self, mock_retrieve_dal):
body = {"action_instance_id": 1, "pipeline_id": 2, "action_id": 3, "status_id": 9, "expression": "x == 1"}
self.request.get_json.return_value = body

self.dal.create_object.return_value = {"id": 456, **body}
mock_retrieve_dal.return_value = self.dal

resp = self.controller.create('action_instance_conditions')

data = json.loads(resp.get_data(as_text=True))
assert data['id'] == 456
assert data['action_instance_id'] == 1
assert data['pipeline_id'] == 2
assert data['action_id'] == 3
assert data['status_id'] == 9
assert data['expression'] == 'x == 1'
self.dal.create_object.assert_called()

@patch.object(APIRouter, APIRouter._retrieve_dal.__name__)
def test_action_instance_conditions_edit(self, mock_retrieve_dal):
self.request.json = {"expression": "x == 3"}

instance = MagicMock()
instance.serialize.return_value = {"id": 456, "expression": "x == 3"}
self.dal.edit_object.return_value = instance
mock_retrieve_dal.return_value = self.dal

resp = self.controller.edit_object('action_instance_conditions', 456)

data = json.loads(resp.get_data(as_text=True))
assert data['id'] == 456
assert data['expression'] == 'x == 3'
self.dal.edit_object.assert_called()

@patch.object(APIRouter, APIRouter._retrieve_dal.__name__)
def test_action_instance_conditions_delete(self, mock_retrieve_dal):
instance = MagicMock()
instance.serialize.return_value = {"id": 456}
self.dal.delete_object.return_value = instance
mock_retrieve_dal.return_value = self.dal

resp = self.controller.delete_object('action_instance_conditions', 456)

data = json.loads(resp.get_data(as_text=True))
assert data['id'] == 456
self.dal.delete_object.assert_called()