diff --git a/backend/alembic/versions/2025_01_07_2208-98a16d1ef6be_adding_new_table_for_canhistory.py b/backend/alembic/versions/2025_01_07_2208-98a16d1ef6be_adding_new_table_for_canhistory.py index 33b8fe4792..b9b9db3383 100644 --- a/backend/alembic/versions/2025_01_07_2208-98a16d1ef6be_adding_new_table_for_canhistory.py +++ b/backend/alembic/versions/2025_01_07_2208-98a16d1ef6be_adding_new_table_for_canhistory.py @@ -27,7 +27,7 @@ def upgrade() -> None: sa.Column('ops_event_id', sa.Integer(), autoincrement=False, nullable=True), sa.Column('history_title', sa.String(), autoincrement=False, nullable=True), sa.Column('history_message', sa.Text(), autoincrement=False, nullable=True), - sa.Column('timestamp', sa.String(), autoincrement=False, nullable=True), + sa.Column('timestamp', sa.DateTime(), autoincrement=False, nullable=True), sa.Column('history_type', postgresql.ENUM('CAN_DATA_IMPORT', 'CAN_NICKNAME_EDITED', 'CAN_DESCRIPTION_EDITED', 'CAN_FUNDING_CREATED', 'CAN_RECEIVED_CREATED', 'CAN_FUNDING_EDITED', 'CAN_RECEIVED_EDITED', 'CAN_FUNDING_DELETED', 'CAN_RECEIVED_DELETED', 'CAN_PORTFOLIO_CREATED', 'CAN_PORTFOLIO_DELETED', 'CAN_PORTFOLIO_EDITED', 'CAN_DIVISION_CREATED', 'CAN_DIVISION_DELETED', 'CAN_DIVISION_EDITED', 'CAN_CARRY_FORWARD_CALCULATED', name='canhistorytype', create_type=False), autoincrement=False, nullable=True), sa.Column('created_by', sa.Integer(), autoincrement=False, nullable=True), sa.Column('updated_by', sa.Integer(), autoincrement=False, nullable=True), @@ -47,7 +47,7 @@ def upgrade() -> None: sa.Column('ops_event_id', sa.Integer(), nullable=False), sa.Column('history_title', sa.String(), nullable=False), sa.Column('history_message', sa.Text(), nullable=False), - sa.Column('timestamp', sa.String(), nullable=False), + sa.Column('timestamp', sa.DateTime(), nullable=False), sa.Column('history_type', postgresql.ENUM('CAN_DATA_IMPORT', 'CAN_NICKNAME_EDITED', 'CAN_DESCRIPTION_EDITED', 'CAN_FUNDING_CREATED', 'CAN_RECEIVED_CREATED', 'CAN_FUNDING_EDITED', 'CAN_RECEIVED_EDITED', 'CAN_FUNDING_DELETED', 'CAN_RECEIVED_DELETED', 'CAN_PORTFOLIO_CREATED', 'CAN_PORTFOLIO_DELETED', 'CAN_PORTFOLIO_EDITED', 'CAN_DIVISION_CREATED', 'CAN_DIVISION_DELETED', 'CAN_DIVISION_EDITED', 'CAN_CARRY_FORWARD_CALCULATED', name='canhistorytype', create_type=False), nullable=True), sa.Column('created_by', sa.Integer(), nullable=True), sa.Column('updated_by', sa.Integer(), nullable=True), diff --git a/backend/alembic/versions/2025_02_06_2133-3d8237681112_adding_fiscal_year_to_canhistory.py b/backend/alembic/versions/2025_02_06_2133-3d8237681112_adding_fiscal_year_to_canhistory.py new file mode 100644 index 0000000000..e36b2f3888 --- /dev/null +++ b/backend/alembic/versions/2025_02_06_2133-3d8237681112_adding_fiscal_year_to_canhistory.py @@ -0,0 +1,31 @@ +"""adding fiscal year to CANHistory + +Revision ID: 3d8237681112 +Revises: c9a2b729be0e +Create Date: 2025-02-06 21:33:56.555892+00:00 + +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = '3d8237681112' +down_revision: Union[str, None] = 'c9a2b729be0e' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('can_history', sa.Column('fiscal_year', sa.Integer(), nullable=False)) + op.add_column('can_history_version', sa.Column('fiscal_year', sa.Integer(), autoincrement=False, nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('can_history_version', 'fiscal_year') + op.drop_column('can_history', 'fiscal_year') + # ### end Alembic commands ### diff --git a/backend/data_tools/data/can_data.json5 b/backend/data_tools/data/can_data.json5 index f88c95f719..2be7b5af0c 100644 --- a/backend/data_tools/data/can_data.json5 +++ b/backend/data_tools/data/can_data.json5 @@ -114,7 +114,7 @@ fiscal_year: 2023, fund_code: "0GXXXX20241RAD", method_of_transfer: "IDDA", - } + }, ], can: [ { @@ -268,498 +268,605 @@ nick_name: "HS CDC", portfolio_id: 2, funding_details_id: 19, - } + }, ], can_funding_budget: [ - { // 1 + { + // 1 fiscal_year: 2023, can_id: 500, budget: 1140000.0, }, - { // 2 + { + // 2 fiscal_year: 2021, can_id: 501, budget: 200000.0, }, - { // 3 + { + // 3 fiscal_year: 2022, can_id: 502, budget: 7000000.0, }, - { // 4 + { + // 4 fiscal_year: 2023, can_id: 502, budget: 10000000.0, }, - { // 5 + { + // 5 fiscal_year: 2024, can_id: 502, budget: 7000000.0, }, - { // 6 + { + // 6 fiscal_year: 2021, can_id: 503, budget: 10000000.0, }, - { // 7 + { + // 7 fiscal_year: 2021, can_id: 504, budget: 10000000.0, }, - { // 8 + { + // 8 fiscal_year: 2022, can_id: 504, budget: 10000000.0, }, - { // 9 + { + // 9 fiscal_year: 2023, can_id: 504, budget: 10000000.0, }, - { // 10 + { + // 10 fiscal_year: 2024, can_id: 504, budget: 10000000.0, }, - { // 11 + { + // 11 fiscal_year: 2021, can_id: 505, budget: 10000000.0, }, - { // 12 + { + // 12 fiscal_year: 2023, can_id: 506, budget: 10000000.0, }, - { // 13 + { + // 13 fiscal_year: 2023, can_id: 501, budget: 10000000.0, }, - { // 14 + { + // 14 fiscal_year: 2023, can_id: 503, budget: 10000000.0, }, - { // 15 + { + // 15 fiscal_year: 2022, can_id: 510, budget: 10000000.0, }, - { // 16 + { + // 16 fiscal_year: 2023, can_id: 510, budget: 10000000.0, }, - { // 17 + { + // 17 fiscal_year: 2022, can_id: 511, budget: 10000000.0, }, - { // 18 + { + // 18 fiscal_year: 2023, can_id: 511, budget: 10000000.0, }, - { // 19 + { + // 19 fiscal_year: 2023, can_id: 512, budget: 1140000.0, }, - { // 20 + { + // 20 fiscal_year: 2024, can_id: 512, budget: 1140000.0, }, - { // 21 + { + // 21 fiscal_year: 2023, can_id: 507, budget: 1140000.0, }, - { // 22 + { + // 22 fiscal_year: 2023, can_id: 508, budget: 1140000.0, }, - { // 23 + { + // 23 fiscal_year: 2023, can_id: 505, budget: 1140000.0, }, - { // 24 + { + // 24 fiscal_year: 2023, can_id: 513, budget: 1000000.0, }, - { // 25 + { + // 25 fiscal_year: 2024, can_id: 514, budget: 1000000.0, }, - { // 26 + { + // 26 fiscal_year: 2023, can_id: 515, budget: 1000000.0, }, - { // 27 + { + // 27 fiscal_year: 2024, can_id: 515, budget: 1000000.0, }, - { // 28 + { + // 28 fiscal_year: 2025, can_id: 515, budget: 1000000.0, }, - { // 29 + { + // 29 fiscal_year: 2026, can_id: 515, budget: 500000.0, }, - { // 30 + { + // 30 fiscal_year: 2027, can_id: 515, budget: 500000.0, }, - { // 31 + { + // 31 fiscal_year: 2023, can_id: 516, budget: 500000.0, }, ], can_funding_received: [ - { // 1 + { + // 1 fiscal_year: 2023, can_id: 500, funding: 880000.0, }, - { // 2 + { + // 2 fiscal_year: 2021, can_id: 501, funding: 200000.0, }, - { // 3 + { + // 3 fiscal_year: 2022, can_id: 502, funding: 6000000.0, }, - { // 4 + { + // 4 fiscal_year: 2023, can_id: 502, funding: 7000000.0, }, - { // 5 + { + // 5 fiscal_year: 2024, can_id: 502, funding: 6000000.0, }, - { // 6 + { + // 6 fiscal_year: 2021, can_id: 503, funding: 6000000.0, }, - { // 7 + { + // 7 fiscal_year: 2021, can_id: 504, funding: 6000000.0, }, - { // 8 + { + // 8 fiscal_year: 2022, can_id: 504, funding: 6000000.0, }, - { // 9 + { + // 9 fiscal_year: 2023, can_id: 504, funding: 6000000.0, }, - { // 10 + { + // 10 fiscal_year: 2024, can_id: 504, funding: 6000000.0, }, - { // 11 + { + // 11 fiscal_year: 2021, can_id: 505, funding: 6000000.0, }, - { // 12 + { + // 12 fiscal_year: 2023, can_id: 506, funding: 6000000.0, }, - { // 13 + { + // 13 fiscal_year: 2023, can_id: 501, funding: 6000000.0, }, - { // 14 + { + // 14 fiscal_year: 2023, can_id: 503, funding: 6000000.0, }, - { // 15 + { + // 15 fiscal_year: 2022, can_id: 510, funding: 6000000.0, }, - { // 16 + { + // 16 fiscal_year: 2023, can_id: 510, funding: 6000000.0, }, - { // 17 + { + // 17 fiscal_year: 2022, can_id: 511, funding: 6000000.0, }, - { // 18 + { + // 18 fiscal_year: 2023, can_id: 511, funding: 6000000.0, }, - { // 19 + { + // 19 fiscal_year: 2023, can_id: 512, funding: 880000.0, }, - { // 20 + { + // 20 fiscal_year: 2024, can_id: 512, funding: 880000.0, }, - { // 21 + { + // 21 fiscal_year: 2023, can_id: 507, funding: 880000.0, }, - { // 22 + { + // 22 fiscal_year: 2023, can_id: 508, funding: 880000.0, }, - { // 23 + { + // 23 fiscal_year: 2023, can_id: 505, funding: 880000.0, }, - { // 24 + { + // 24 fiscal_year: 2023, can_id: 513, funding: 1000000.0, }, - { // 25 + { + // 25 fiscal_year: 2024, can_id: 514, funding: 1000000.0, }, - { // 26 + { + // 26 fiscal_year: 2023, can_id: 515, funding: 1000000.0, }, ], can_history: [ - { // 1 + { + // 1 can_id: 500, ops_event_id: 1, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 2 + { + // 2 can_id: 501, ops_event_id: 2, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-01T00:07:00.000000Z", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 3 + { + // 3 can_id: 502, ops_event_id: 3, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 4 + { + // 4 can_id: 503, ops_event_id: 4, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 5 + { + // 5 can_id: 504, ops_event_id: 5, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 6 + { + // 6 can_id: 505, ops_event_id: 6, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 7 + { + // 7 can_id: 506, ops_event_id: 7, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 8 + { + // 8 can_id: 507, ops_event_id: 8, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 9 + { + // 9 can_id: 508, ops_event_id: 9, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 10 + { + // 10 can_id: 509, ops_event_id: 10, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 11 + { + // 11 can_id: 510, ops_event_id: 11, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 12 + { + // 12 can_id: 511, ops_event_id: 12, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 13 + { + // 13 can_id: 512, ops_event_id: 13, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 14 + { + // 14 can_id: 513, ops_event_id: 14, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 15 + { + // 15 can_id: 514, ops_event_id: 15, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 16 + { + // 16 can_id: 515, ops_event_id: 16, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 17 + { + // 17 can_id: 516, ops_event_id: 17, history_title: "FY 2026 Data Import", history_message: "FY 2026 CAN Funding Information imported from CANBACs", timestamp: "2025-10-02T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2026 }, - { // 18 + { + // 18 can_id: 500, ops_event_id: 18, history_title: "Description Edited", history_message: "Steve Tekell edited the description", timestamp: "2025-01-01T00:08:00.000000Z", - history_type: "CAN_DESCRIPTION_EDITED" + history_type: "CAN_DESCRIPTION_EDITED", + fiscal_year: 2025 }, - { // 19 + { + // 19 can_id: 500, ops_event_id: 19, history_title: "Description Edited", history_message: "Steve Tekell edited the description", timestamp: "2025-01-01T00:09:00.000000Z", - history_type: "CAN_DESCRIPTION_EDITED" + history_type: "CAN_DESCRIPTION_EDITED", + fiscal_year: 2025 }, - { // 20 + { + // 20 can_id: 500, ops_event_id: 20, history_title: "FY 2025 Budget Entered", history_message: "Steve Tekell entered a FY 2025 budget of $10,000.00", timestamp: "2025-01-01T00:10:00.000000Z", - history_type: "CAN_FUNDING_CREATED" + history_type: "CAN_FUNDING_CREATED", + fiscal_year: 2025 }, - { // 21 + { + // 21 can_id: 500, ops_event_id: 21, history_title: "Funding Received Added", history_message: "Steve Tekell added funding received to funding ID 526 in the amount of $250,000.00", timestamp: "2025-01-01T00:11:00.000000Z", - history_type: "CAN_RECEIVED_CREATED" + history_type: "CAN_RECEIVED_CREATED", + fiscal_year: 2025 }, - { // 22 + { + // 22 can_id: 500, ops_event_id: 22, history_title: "FY 2025 Budget Edited", history_message: "Steve Tekell edited the FY 2025 budget from $1,000,000.00 to $1,140,000.00", timestamp: "2025-01-01T00:11:30.000000Z", - history_type: "CAN_FUNDING_EDITED" + history_type: "CAN_FUNDING_EDITED", + fiscal_year: 2025 }, - { // 23 + { + // 23 can_id: 500, ops_event_id: 23, history_title: "Funding Received Edited", history_message: "Steve Tekell edited funding received for funding ID 500 from $880,000.00 to $1,000,000.00", timestamp: "2025-01-01T00:12:00.000000Z", - history_type: "CAN_RECEIVED_EDITED" + history_type: "CAN_RECEIVED_EDITED", + fiscal_year: 2025 }, - { // 24 + { + // 24 can_id: 500, ops_event_id: 25, history_title: "Funding Received Deleted", history_message: "Steve Tekell deleted funding received for funding ID 526 in the amount of $1000.00", timestamp: "2025-01-01T00:12:30.000000Z", - history_type: "CAN_RECEIVED_DELETED" + history_type: "CAN_RECEIVED_DELETED", + fiscal_year: 2025 }, - { // 25 + { + // 25 can_id: 500, ops_event_id: 25, history_title: "FY 2025 Budget Entered", history_message: "Steve Tekell entered a FY 2025 budget of $30,000.00", timestamp: "2025-01-01T00:10:00.000000Z", - history_type: "CAN_FUNDING_CREATED" + history_type: "CAN_FUNDING_CREATED", + fiscal_year: 2025 }, { // 26 @@ -768,7 +875,8 @@ history_title: "Nickname Edited", history_message: "Steve Tekell edited the nickname from New CAN to HMRF-OPRE", timestamp: "2025-01-29T18:43:53.928887Z", - history_type: "CAN_NICKNAME_EDITED" + history_type: "CAN_NICKNAME_EDITED", + fiscal_year: 2025 }, { // 27 @@ -777,25 +885,68 @@ history_title: "Description Edited", history_message: "Steve Tekell edited the description", timestamp: "2025-01-29T18:43:53.928887Z", - history_type: "CAN_DESCRIPTION_EDITED" + history_type: "CAN_DESCRIPTION_EDITED", + fiscal_year: 2025 }, { // 28 can_id: 500, ops_event_id: 27, history_title: "CAN Portfolio Edited", - history_message: "CAN portfolio changed from Child Welfare Research to Healthy Marriage & Responsible Fatherhood during FY 2025 data import", - timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_PORTFOLIO_EDITED" + history_message: "Steve Tekell changed portfolio from Welfare Research to Child Welfare Research", + timestamp: "2025-01-1T21:42:53.928887Z", + history_type: "CAN_PORTFOLIO_EDITED", + fiscal_year: 2025 }, { // 29 can_id: 500, + ops_event_id: 27, + history_title: "CAN Division Edited", + history_message: "Steve Tekell changed division from Division of Economic Independence to Division of Child and Family Development", + timestamp: "2025-01-13T21:42:53.928887Z", + history_type: "CAN_DIVISION_EDITED", + fiscal_year: 2025 + }, + { + // 30 + can_id: 500, ops_event_id: 28, history_title: "FY 2025 Budget Entered", history_message: "Steve Tekell entered a FY 2025 budget of $10,000.00", + timestamp: "2025-01-15T21:42:53.928887Z", + history_type: "CAN_FUNDING_CREATED", + fiscal_year: 2025 + }, + { + // 31 + can_id: 500, + ops_event_id: 29, + history_title: "CAN Portfolio Edited", + history_message: "Portfolio changed from Child Welfare Research to Healthy Marriage & Responsible Fatherhood during FY 2025 data import", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_FUNDING_CREATED" - } - ] + history_type: "CAN_PORTFOLIO_EDITED", + fiscal_year: 2025 + }, + { + // 32 + can_id: 500, + ops_event_id: 29, + history_title: "CAN Division Edited", + history_message: "Division changed from Division of Child and Family Development to Division of Family Strengthening during FY 2025 data import", + timestamp: "2025-01-13T21:42:53.928887Z", + history_type: "CAN_DIVISION_EDITED", + fiscal_year: 2025 + }, + { + // 33 + can_id: 501, + ops_event_id: 30, + history_title: "Nickname Edited", + history_message: "Nickname changed from Interagency Agreement to IAA-Incoming during FY 2025 data import", + timestamp: "2025-02-04T21:42:53.928887Z", + history_type: "CAN_NICKNAME_EDITED", + fiscal_year: 2025 + }, + ], } diff --git a/backend/data_tools/data/ops_event.json5 b/backend/data_tools/data/ops_event.json5 index e753c7ad56..523d14e071 100644 --- a/backend/data_tools/data/ops_event.json5 +++ b/backend/data_tools/data/ops_event.json5 @@ -2669,8 +2669,8 @@ can_id: 500, changes: { portfolio_id: { - new_value: 6, - old_value: 1, + new_value: 1, + old_value: 4, }, }, updated_by: 516, @@ -2796,5 +2796,79 @@ created_on: "2025-01-13T21:42:53.928887Z", created_by: 516, }, + { + // 29 + event_type: "UPDATE_CAN", + event_status: "SUCCESS", + event_details: { + can_updates: { + can_id: 500, + changes: { + portfolio_id: { + new_value: 6, + old_value: 1, + }, + }, + updated_by: 526, + }, + "request.json": { + portfolio_id: 6, + }, + "request.values": {}, + "request.headers": { + Host: "localhost:8080", + Accept: "*/*", + Connection: "keep-alive", + "User-Agent": "PostmanRuntime/7.43.0", + "Content-Type": "application/json", + Authorization: "Bearer token", + "Cache-Control": "no-cache", + "Postman-Token": "3a2d7d8b-3f35-4ce2-8983-dfdafc1ee97f", + "Content-Length": "25", + "Accept-Encoding": "gzip, deflate, br", + }, + "request.remote_addr": "192.168.127.1", + "request.remote_user": null, + }, + created_by: 526, + created_on: "2025-01-29T19:44:53.928887Z", + }, + { + // 30 + event_type: "UPDATE_CAN", + event_status: "SUCCESS", + event_details: { + can_updates: { + can_id: 501, + changes: { + nick_name: { + new_value: "IAA-Incoming", + old_value: "Interagency Agreements", + }, + }, + updated_by: 526, + }, + "request.json": { + nick_name: "IAA-Incoming", + }, + "request.values": {}, + "request.headers": { + Host: "localhost:8080", + Accept: "*/*", + Connection: "keep-alive", + "User-Agent": "PostmanRuntime/7.43.0", + "Content-Type": "application/json", + Authorization: "Bearer token", + "Cache-Control": "no-cache", + "Postman-Token": "ffe942f3-5355-4651-bb5b-80f2cfc851bf", + "Content-Length": "48", + "Accept-Encoding": "gzip, deflate, br", + }, + "request.remote_addr": "192.168.127.1", + "request.remote_user": null, + }, + created_by: 526, + created_on: "2025-02-04T14:43:53.928887Z", + }, ], } diff --git a/backend/data_tools/src/load_cans/utils.py b/backend/data_tools/src/load_cans/utils.py index 21826f1519..c9d29d96ae 100644 --- a/backend/data_tools/src/load_cans/utils.py +++ b/backend/data_tools/src/load_cans/utils.py @@ -177,7 +177,7 @@ def create_models(data: CANData, sys_user: User, session: Session) -> None: session.add(event) session.commit() - can_history_trigger_func(event, session) + can_history_trigger_func(event, session, sys_user) except Exception as e: logger.error(f"Error creating models for {data}") raise e diff --git a/backend/models/can_history.py b/backend/models/can_history.py index c9e444f8ac..84cc977864 100644 --- a/backend/models/can_history.py +++ b/backend/models/can_history.py @@ -2,7 +2,7 @@ from enum import Enum, auto from loguru import logger -from sqlalchemy import ForeignKey, Integer, Text +from sqlalchemy import ForeignKey, Integer, Text, func from sqlalchemy.dialects.postgresql import ENUM from sqlalchemy.orm import Mapped, Session, mapped_column @@ -39,15 +39,17 @@ class CANHistory(BaseModel): ops_event_id: Mapped[int] = mapped_column(Integer, ForeignKey("ops_event.id")) history_title: Mapped[str] history_message: Mapped[str] = mapped_column(Text) - timestamp: Mapped[str] + timestamp: Mapped[datetime] = mapped_column(default=func.now()) history_type: Mapped[CANHistoryType] = mapped_column( ENUM(CANHistoryType), nullable=True ) + fiscal_year: Mapped[int] def can_history_trigger_func( event: OpsEvent, session: Session, + system_user: User, ): # Do not attempt to insert events into CAN History for failed or unknown status events if event.event_status == OpsEventStatus.FAILED or event.event_status == OpsEventStatus.UNKNOWN: @@ -60,21 +62,22 @@ def can_history_trigger_func( match event.event_type: case OpsEventType.CREATE_NEW_CAN: - fiscal_year = format_fiscal_year(event.event_details["new_can"]["created_on"]) + current_fiscal_year = format_fiscal_year(event.event_details["new_can"]["created_on"]) history_event = CANHistory( can_id=event.event_details["new_can"]["id"], ops_event_id=event.id, - history_title=f"{fiscal_year} Data Import", - history_message=f"{fiscal_year} CAN Funding Information imported from CANBACs", + history_title=f"FY {current_fiscal_year} Data Import", + history_message=f"FY {current_fiscal_year} CAN Funding Information imported from CANBACs", timestamp=event.created_on, history_type=CANHistoryType.CAN_DATA_IMPORT, + fiscal_year=current_fiscal_year ) session.add(history_event) case OpsEventType.UPDATE_CAN: # Handle CAN Updates change_dict = event.event_details["can_updates"]["changes"] for key in change_dict.keys(): - history_event = create_can_update_history_event( + create_can_update_history_event( key, change_dict[key]["old_value"], change_dict[key]["new_value"], @@ -83,9 +86,8 @@ def can_history_trigger_func( event.event_details["can_updates"]["can_id"], event.id, session, + system_user ) - if history_event is not None: - session.add(history_event) case OpsEventType.CREATE_CAN_FUNDING_BUDGET: current_fiscal_year = format_fiscal_year(event.event_details["new_can_funding_budget"]["created_on"]) budget = "${:,.2f}".format(event.event_details["new_can_funding_budget"]["budget"]) @@ -93,10 +95,11 @@ def can_history_trigger_func( history_event = CANHistory( can_id=event.event_details["new_can_funding_budget"]["can"]["id"], ops_event_id=event.id, - history_title=f"{current_fiscal_year} Budget Entered", - history_message=f"{creator_name} entered a {current_fiscal_year} budget of {budget}", + history_title=f"FY {current_fiscal_year} Budget Entered", + history_message=f"{creator_name} entered a FY {current_fiscal_year} budget of {budget}", timestamp=event.created_on, history_type=CANHistoryType.CAN_FUNDING_CREATED, + fiscal_year= current_fiscal_year ) session.add(history_event) case OpsEventType.UPDATE_CAN_FUNDING_BUDGET: @@ -110,14 +113,16 @@ def can_history_trigger_func( history_event = CANHistory( can_id=event.event_details["funding_budget_updates"]["can_id"], ops_event_id=event.id, - history_title=f"{current_fiscal_year} Budget Edited", - history_message=f"{event_user.full_name} edited the {current_fiscal_year} budget from {old_budget} to {new_budget}", + history_title=f"FY {current_fiscal_year} Budget Edited", + history_message=f"{event_user.full_name} edited the FY {current_fiscal_year} budget from {old_budget} to {new_budget}", timestamp=event.created_on, history_type=CANHistoryType.CAN_FUNDING_EDITED, + fiscal_year=current_fiscal_year ) session.add(history_event) case OpsEventType.CREATE_CAN_FUNDING_RECEIVED: funding = "${:,.2f}".format(event.event_details["new_can_funding_received"]["funding"]) + current_fiscal_year = format_fiscal_year(event.event_details["new_can_funding_received"]["created_on"]) creator_name = f"{event_user.full_name}" history_event = CANHistory( can_id=event.event_details["new_can_funding_received"]["can_id"], @@ -126,10 +131,12 @@ def can_history_trigger_func( history_message=f"{creator_name} added funding received to funding ID {event.event_details['new_can_funding_received']['id']} in the amount of {funding}", timestamp=event.created_on, history_type=CANHistoryType.CAN_RECEIVED_CREATED, + fiscal_year=current_fiscal_year ) session.add(history_event) case OpsEventType.UPDATE_CAN_FUNDING_RECEIVED: changes = event.event_details["funding_received_updates"]["changes"] + current_fiscal_year = format_fiscal_year(event.created_on) if "funding" in changes: funding_changes = changes["funding"] old_funding = "${:,.2f}".format(funding_changes["old_value"]) @@ -141,10 +148,12 @@ def can_history_trigger_func( history_message=f"{event_user.full_name} edited funding received for funding ID {event.event_details['funding_received_updates']['funding_id']} from {old_funding} to {new_funding}", timestamp=event.created_on, history_type=CANHistoryType.CAN_RECEIVED_EDITED, + fiscal_year=current_fiscal_year ) session.add(history_event) case OpsEventType.DELETE_CAN_FUNDING_RECEIVED: funding = "${:,.2f}".format(event.event_details["deleted_can_funding_received"]["funding"]) + current_fiscal_year = format_fiscal_year(event.event_details["deleted_can_funding_received"]["created_on"]) creator_name = f"{event_user.full_name}" history_event = CANHistory( can_id=event.event_details["deleted_can_funding_received"]["can_id"], @@ -153,65 +162,82 @@ def can_history_trigger_func( history_message=f"{creator_name} deleted funding received for funding ID {event.event_details['deleted_can_funding_received']['id']} in the amount of {funding}", timestamp=event.created_on, history_type=CANHistoryType.CAN_RECEIVED_DELETED, + fiscal_year=current_fiscal_year ) session.add(history_event) session.commit() def format_fiscal_year(timestamp): - """Convert the timestamp to FY {Fiscal Year}. The fiscal year is calendar year + 1 if the timestamp is october or later. + """Convert the timestamp to {Fiscal Year}. The fiscal year is calendar year + 1 if the timestamp is october or later. This method can take either an iso format timestamp string or a datetime object""" - current_fiscal_year = "FY" + current_fiscal_year = 0 if isinstance(timestamp, str): parsed_timestamp = datetime.fromisoformat(timestamp[:-1]).astimezone(timezone.utc) - current_fiscal_year = f"FY {parsed_timestamp.year}" + current_fiscal_year = parsed_timestamp.year if parsed_timestamp.month >= 10: - current_fiscal_year = f"FY {parsed_timestamp.year + 1}" + current_fiscal_year = parsed_timestamp.year + 1 elif isinstance(timestamp, datetime): if timestamp.month >= 10: - current_fiscal_year = f"FY {timestamp.year + 1}" + current_fiscal_year = timestamp.year + 1 else: - current_fiscal_year = f"FY {timestamp.year}" + current_fiscal_year = timestamp.year return current_fiscal_year def create_can_update_history_event( - property_name, old_value, new_value, updated_by_user, updated_on, can_id, ops_event_id, session + property_name, old_value, new_value, updated_by_user, updated_on, can_id, ops_event_id, session, sys_user ): """A method that generates a CANHistory event for an updated property. In the case where the updated property is not one that has been designed for, it will instead be logged and None will be returned from the method.""" + + updated_by_sys_user = sys_user.id == updated_by_user.id + + current_fiscal_year = format_fiscal_year(updated_on) match property_name: case "nick_name": - return CANHistory( + session.add(CANHistory( can_id=can_id, ops_event_id=ops_event_id, history_title="Nickname Edited", - history_message=f"{updated_by_user.full_name} edited the nickname from {old_value} to {new_value}", + history_message=f"Nickname changed from {old_value} to {new_value} during FY {current_fiscal_year} data import" if updated_by_sys_user else f"{updated_by_user.full_name} edited the nickname from {old_value} to {new_value}", timestamp=updated_on, history_type=CANHistoryType.CAN_NICKNAME_EDITED, - ) + fiscal_year=current_fiscal_year + )) case "description": - return CANHistory( + session.add(CANHistory( can_id=can_id, ops_event_id=ops_event_id, history_title="Description Edited", history_message=f"{updated_by_user.full_name} edited the description", timestamp=updated_on, history_type=CANHistoryType.CAN_DESCRIPTION_EDITED, - ) + fiscal_year=current_fiscal_year + )) case "portfolio_id": old_portfolio = session.get(Portfolio, old_value) new_portfolio = session.get(Portfolio, new_value) - current_fiscal_year = format_fiscal_year(updated_on) - return CANHistory( + session.add(CANHistory( can_id=can_id, ops_event_id=ops_event_id, history_title="CAN Portfolio Edited", - history_message=f"CAN portfolio changed from {old_portfolio.name} to {new_portfolio.name} during {current_fiscal_year} data import", + history_message=f"CAN portfolio changed from {old_portfolio.name} to {new_portfolio.name} during FY {current_fiscal_year} data import" if updated_by_sys_user else f"{updated_by_user.full_name} changed the portfolio from {old_portfolio.name} to {new_portfolio.name}", timestamp=updated_on, history_type=CANHistoryType.CAN_PORTFOLIO_EDITED, - ) + fiscal_year=current_fiscal_year + )) + if old_portfolio.division_id != new_portfolio.division_id: + session.add(CANHistory( + can_id=can_id, + ops_event_id=ops_event_id, + history_title="CAN Division Edited", + history_message=f"CAN division changed from {old_portfolio.division.name} to {new_portfolio.division.name} during FY {current_fiscal_year} data import" if updated_by_sys_user else f"{updated_by_user.full_name} changed the division from {old_portfolio.division.name} to {new_portfolio.division.name}", + timestamp=updated_on, + history_type=CANHistoryType.CAN_DIVISION_EDITED, + fiscal_year=current_fiscal_year + )) case _: logger.info(f"{property_name} edited by {updated_by_user.full_name} from {old_value} to {new_value}") return None diff --git a/backend/openapi.yml b/backend/openapi.yml index 7dc811d05e..f9a49e8d64 100644 --- a/backend/openapi.yml +++ b/backend/openapi.yml @@ -882,7 +882,7 @@ paths: tags: - CAN History operationId: getAllCANHistory - summary: Get a list all CAN history objects + summary: Get a list all CAN history objects, sorted by their timestamps from newest to oldest parameters: - $ref: "#/components/parameters/simulatedError" - name: can_id @@ -901,6 +901,17 @@ paths: type: integer default: 10 example: 10 + - name: fiscal_year + in: query + schema: + type: integer + example: 2025 + - name: sort_asc + in: query + description: Optional parameter that sets whether to sort the results in ascending (oldest to newest) order + schema: + type: boolean + example: true description: Get CANHistory responses: "200": diff --git a/backend/ops_api/ops/resources/can_history.py b/backend/ops_api/ops/resources/can_history.py index 5bbcf58eb5..02e908c6ff 100644 --- a/backend/ops_api/ops/resources/can_history.py +++ b/backend/ops_api/ops/resources/can_history.py @@ -18,6 +18,8 @@ def __init__(self, model): @error_simulator def get(self) -> Response: data = self._get_schema.dump(self._get_schema.load(request.args)) - result = self.service.get(data.get("can_id"), data.get("limit"), data.get("offset")) + result = self.service.get( + data.get("can_id"), data.get("limit"), data.get("offset"), data.get("fiscal_year"), data.get("sort_asc") + ) can_history_schema = CANHistoryItemSchema() return make_response_with_headers([can_history_schema.dump(funding_budget) for funding_budget in result]) diff --git a/backend/ops_api/ops/schemas/can_history.py b/backend/ops_api/ops/schemas/can_history.py index 4ae6e123c2..d94d3ad3b1 100644 --- a/backend/ops_api/ops/schemas/can_history.py +++ b/backend/ops_api/ops/schemas/can_history.py @@ -18,5 +18,7 @@ class Meta: unknown = EXCLUDE # Exclude unknown fields can_id = fields.Integer(required=True) + fiscal_year = fields.Integer(default=0) limit = fields.Integer(default=10, validate=Range(min=1, error="Limit must be greater than 0"), allow_none=True) offset = fields.Integer(default=0, validate=Range(min=0, error="Limit must be greater than 0"), allow_none=True) + sort_asc = fields.Boolean(default=False) diff --git a/backend/ops_api/ops/services/can_history.py b/backend/ops_api/ops/services/can_history.py index 3e745fd6e3..5e2a2875c6 100644 --- a/backend/ops_api/ops/services/can_history.py +++ b/backend/ops_api/ops/services/can_history.py @@ -5,10 +5,17 @@ class CANHistoryService: - def get(self, can_id, limit, offset) -> list[CANHistory]: + def get(self, can_id, limit, offset, fiscal_year, sort_ascending=False) -> list[CANHistory]: """ Get a list of CAN History items for an individual can. """ - stmt = select(CANHistory).where(CANHistory.can_id == can_id).order_by(CANHistory.id).offset(offset).limit(limit) + stmt = select(CANHistory).where(CANHistory.can_id == can_id) + if fiscal_year > 0: + stmt = stmt.where(CANHistory.fiscal_year == fiscal_year) + if sort_ascending: + stmt = stmt.order_by(CANHistory.timestamp) + else: + stmt = stmt.order_by(CANHistory.timestamp.desc()) + stmt = stmt.offset(offset).limit(limit) results = current_app.db_session.execute(stmt).all() return [can_history for result in results for can_history in result] diff --git a/backend/ops_api/ops/services/can_messages.py b/backend/ops_api/ops/services/can_messages.py index 72564fab10..c43a804ce7 100644 --- a/backend/ops_api/ops/services/can_messages.py +++ b/backend/ops_api/ops/services/can_messages.py @@ -2,6 +2,7 @@ from sqlalchemy.orm import Session from models import OpsEvent, can_history_trigger_func +from ops_api.ops.utils.users import get_sys_user def can_history_trigger( @@ -9,6 +10,7 @@ def can_history_trigger( session: Session, ): try: - can_history_trigger_func(event, session) + sys_user = get_sys_user(session) + can_history_trigger_func(event, session, sys_user) except Exception as e: logger.error(f"Error in can_history_trigger: {e}") diff --git a/backend/ops_api/ops/utils/users.py b/backend/ops_api/ops/utils/users.py index 99dd447641..ea8c89dd58 100644 --- a/backend/ops_api/ops/utils/users.py +++ b/backend/ops_api/ops/utils/users.py @@ -4,6 +4,8 @@ from models import Role, User +SYSTEM_ADMIN_OIDC_ID = "00000000-0000-1111-a111-000000000026" + def is_user_admin(user: User, session: Session = None) -> bool: if not session: @@ -11,3 +13,17 @@ def is_user_admin(user: User, session: Session = None) -> bool: user_admin_role = session.execute(select(Role).where(Role.name == "USER_ADMIN")).scalar_one() return user_admin_role in user.roles + + +def get_sys_user(session: Session) -> User: + """ + Get or create the system user. + + Args: + session: SQLAlchemy session object + Returns: + None + """ + user = session.execute(select(User).where(User.oidc_id == SYSTEM_ADMIN_OIDC_ID)).scalar_one_or_none() + + return user diff --git a/backend/ops_api/tests/ops/can_history/test_can_history.py b/backend/ops_api/tests/ops/can_history/test_can_history.py index 2d46ebdfbc..b7ef946b48 100644 --- a/backend/ops_api/tests/ops/can_history/test_can_history.py +++ b/backend/ops_api/tests/ops/can_history/test_can_history.py @@ -12,7 +12,7 @@ def test_get_can_history(loaded_db): count = loaded_db.query(CANHistory).where(CANHistory.can_id == test_can_id).count() can_history_service = CANHistoryService() # Set a limit higher than our test data so we can get all results - response = can_history_service.get(test_can_id, 1000, 0) + response = can_history_service.get(test_can_id, 1000, 0, 2025) assert len(response) == count @@ -22,7 +22,7 @@ def test_get_can_history_custom_length(): test_limit = 5 can_history_service = CANHistoryService() # Set a limit higher than our test data so we can get all results - response = can_history_service.get(test_can_id, test_limit, 0) + response = can_history_service.get(test_can_id, test_limit, 0, 2025) assert len(response) == test_limit @@ -31,15 +31,28 @@ def test_get_can_history_custom_offset(): test_can_id = 500 can_history_service = CANHistoryService() # Set a limit higher than our test data so we can get all results - response = can_history_service.get(test_can_id, 4, 1) - offset_first_CAN = response[0] - offset_second_CAN = response[1] + response = can_history_service.get(test_can_id, 4, 1, 2025) + offset_first_CAN = response[0] # CANHistory#26 + offset_second_CAN = response[1] # CANHistory#28 # The CAN with ID 500 has ops events with id 1, then starting at ops event id 18 and moving forward # Therefore, we expect the ops_event_id to be 18 for the first item in the list offset by 1 - assert offset_first_CAN.ops_event_id == 18 - assert offset_first_CAN.history_type == CANHistoryType.CAN_DESCRIPTION_EDITED - assert offset_second_CAN.ops_event_id == 19 - assert offset_second_CAN.history_type == CANHistoryType.CAN_DESCRIPTION_EDITED + assert offset_first_CAN.ops_event_id == 26 + assert offset_first_CAN.history_type == CANHistoryType.CAN_NICKNAME_EDITED + assert offset_second_CAN.ops_event_id == 28 + assert offset_second_CAN.history_type == CANHistoryType.CAN_FUNDING_CREATED + + +@pytest.mark.usefixtures("app_ctx") +def test_get_can_history_custom_fiscal_year(): + test_can_id = 516 + can_history_service = CANHistoryService() + # Set a fiscal year which returns no cans + response = can_history_service.get(test_can_id, 5, 0, 2025) + assert len(response) == 0 + + # Set a fiscal year which returns one can + response_2 = can_history_service.get(test_can_id, 5, 0, 2026) + assert len(response_2) == 1 @pytest.mark.usefixtures("app_ctx") @@ -47,10 +60,25 @@ def test_get_can_history_nonexistent_can(): test_can_id = 300 can_history_service = CANHistoryService() # Try to get a non-existent CAN and return an empty result instead of throwing any errors. - response = can_history_service.get(test_can_id, 10, 0) + response = can_history_service.get(test_can_id, 10, 0, 2025) assert len(response) == 0 +@pytest.mark.usefixtures("app_ctx") +def test_get_can_history_ascending_sort(): + test_can_id = 501 + can_history_service = CANHistoryService() + ascending_sort_response = can_history_service.get(test_can_id, 10, 0, 2025, True) + oldest_can_history_event = ascending_sort_response[0] + assert len(ascending_sort_response) == 2 + assert oldest_can_history_event.history_type == CANHistoryType.CAN_DATA_IMPORT + + descending_sort_response = can_history_service.get(test_can_id, 10, 0, 2025, False) + assert len(descending_sort_response) == 2 + newest_can_history_event = descending_sort_response[0] + assert newest_can_history_event.history_type == CANHistoryType.CAN_NICKNAME_EDITED + + @pytest.mark.usefixtures("app_ctx") def test_get_can_history_list_from_api(auth_client, mocker): test_can_id = 500 @@ -65,13 +93,14 @@ def test_get_can_history_list_from_api(auth_client, mocker): history_message="CAN Imported by Reed on Wednesdsay January 1st", timestamp="2025-01-01T00:07:00.000000Z", history_type=CANHistoryType.CAN_DATA_IMPORT, + fiscal_year=2025, ) ) mocker_get_can_history = mocker.patch("ops_api.ops.services.can_history.CANHistoryService.get") mocker_get_can_history.return_value = mock_can_history_list - response = auth_client.get(f"/api/v1/can-history/?can_id={test_can_id}") + response = auth_client.get(f"/api/v1/can-history/?can_id={test_can_id}&fiscal_year=2025") assert response.status_code == 200 assert len(response.json) == 10 @@ -90,13 +119,14 @@ def test_get_can_history_list_from_api_with_params(auth_client, mocker): history_message="CAN Imported by Reed on Wednesdsay January 1st", timestamp="2025-01-01T00:07:00.000000Z", history_type=CANHistoryType.CAN_DATA_IMPORT, + fiscal_year=2025, ) ) mocker_get_can_history = mocker.patch("ops_api.ops.services.can_history.CANHistoryService.get") mocker_get_can_history.return_value = mock_can_history_list - response = auth_client.get(f"/api/v1/can-history/?can_id={test_can_id}&limit=5&offset=1") + response = auth_client.get(f"/api/v1/can-history/?can_id={test_can_id}&fiscal_year=2025&limit=5&offset=1") assert response.status_code == 200 assert len(response.json) == 5 @@ -104,14 +134,14 @@ def test_get_can_history_list_from_api_with_params(auth_client, mocker): @pytest.mark.usefixtures("app_ctx") def test_get_can_history_list_from_api_with_bad_limit(auth_client): test_can_id = 500 - response = auth_client.get(f"/api/v1/can-history/?can_id={test_can_id}&limit=0") + response = auth_client.get(f"/api/v1/can-history/?can_id={test_can_id}&fiscal_year=2025&limit=0") assert response.status_code == 400 @pytest.mark.usefixtures("app_ctx") def test_get_can_history_list_from_api_with_bad_offset(auth_client): test_can_id = 500 - response = auth_client.get(f"/api/v1/can-history/?can_id={test_can_id}&offset=-1") + response = auth_client.get(f"/api/v1/can-history/?can_id={test_can_id}&fiscal_year=2025&offset=-1") assert response.status_code == 400 @@ -121,6 +151,28 @@ def test_get_can_history_list_from_api_with_no_can_id(auth_client): assert response.status_code == 400 +@pytest.mark.usefixtures("app_ctx") +def test_get_can_history_from_api_no_fiscal_year(auth_client, mocker): + test_can_id = 500 + mock_can_history_list = [] + mocker_get_can_history = mocker.patch("ops_api.ops.services.can_history.CANHistoryService.get") + mocker_get_can_history.return_value = mock_can_history_list + response = auth_client.get(f"/api/v1/can-history/?can_id={test_can_id}") + mocker_get_can_history.assert_called_once_with(test_can_id, 10, 0, 0, False) + assert response.status_code == 200 + + +@pytest.mark.usefixtures("app_ctx") +def test_get_can_history_from_api_asc_sort(auth_client, mocker): + test_can_id = 500 + mock_can_history_list = [] + mocker_get_can_history = mocker.patch("ops_api.ops.services.can_history.CANHistoryService.get") + mocker_get_can_history.return_value = mock_can_history_list + response = auth_client.get(f"/api/v1/can-history/?can_id={test_can_id}&sort_asc=true") + mocker_get_can_history.assert_called_once_with(test_can_id, 10, 0, 0, True) + assert response.status_code == 200 + + @pytest.mark.usefixtures("app_ctx") def test_get_can_history_list_from_api_with_nonexistent_can(auth_client, mocker): test_can_id = 400 @@ -141,14 +193,15 @@ def test_create_can_can_history_event(loaded_db, test_create_can_history_item): after_can_history_count = len(can_history_list) assert after_can_history_count == before_can_history_count + 1 - new_can_history_item = can_history_list[after_can_history_count - 1] + new_can_history_item = can_history_list[0] event_details = test_create_can_history_item.event_details assert new_can_history_item.can_id == event_details["new_can"]["id"] assert new_can_history_item.ops_event_id == test_create_can_history_item.id assert new_can_history_item.history_type == CANHistoryType.CAN_DATA_IMPORT assert new_can_history_item.history_title == "FY 2025 Data Import" assert new_can_history_item.history_message == "FY 2025 CAN Funding Information imported from CANBACs" - assert new_can_history_item.timestamp == test_create_can_history_item.created_on.strftime("%Y-%m-%d %H:%M:%S.%f") + assert new_can_history_item.timestamp == test_create_can_history_item.created_on + assert new_can_history_item.fiscal_year == 2025 @pytest.mark.usefixtures("app_ctx") @@ -178,7 +231,7 @@ def test_create_can_history_create_can_funding_budget(loaded_db): assert ( new_can_history_item.can_id == funding_budget_created_event.event_details["new_can_funding_budget"]["can"]["id"] ) - assert new_can_history_item.timestamp == funding_budget_created_event.created_on.strftime("%Y-%m-%d %H:%M:%S.%f") + assert new_can_history_item.timestamp == funding_budget_created_event.created_on funding_budget_created_event_2 = loaded_db.get(OpsEvent, 25) can_history_trigger(funding_budget_created_event_2, loaded_db) @@ -193,9 +246,7 @@ def test_create_can_history_create_can_funding_budget(loaded_db): new_can_history_item_2.can_id == funding_budget_created_event_2.event_details["new_can_funding_budget"]["can"]["id"] ) - assert new_can_history_item_2.timestamp == funding_budget_created_event_2.created_on.strftime( - "%Y-%m-%d %H:%M:%S.%f" - ) + assert new_can_history_item_2.timestamp == funding_budget_created_event_2.created_on @pytest.mark.usefixtures("app_ctx") @@ -216,7 +267,7 @@ def test_create_create_can_funding_received(loaded_db): new_can_history_item.can_id == funding_received_created_event.event_details["new_can_funding_received"]["can_id"] ) - assert new_can_history_item.timestamp == funding_received_created_event.created_on.strftime("%Y-%m-%d %H:%M:%S.%f") + assert new_can_history_item.timestamp == funding_received_created_event.created_on @pytest.mark.usefixtures("app_ctx") @@ -237,7 +288,7 @@ def test_create_can_history_delete_can_funding_received(loaded_db): new_can_history_item.can_id == funding_received_deleted_event.event_details["deleted_can_funding_received"]["can_id"] ) - assert new_can_history_item.timestamp == funding_received_deleted_event.created_on.strftime("%Y-%m-%d %H:%M:%S.%f") + assert new_can_history_item.timestamp == funding_received_deleted_event.created_on @pytest.mark.usefixtures("app_ctx") @@ -296,19 +347,73 @@ def test_update_can_funding_received_can_history(loaded_db): @pytest.mark.usefixtures("app_ctx") -def test_update_can_portfolio_can_history(loaded_db): +def test_update_can_portfolio_can_history_regular_user(loaded_db): update_can_event = loaded_db.get(OpsEvent, 27) can_history_trigger(update_can_event, loaded_db) can_update_history_events = ( loaded_db.execute(select(CANHistory).where(CANHistory.ops_event_id == 27)).scalars().all() ) - assert len(can_update_history_events) == 2 + assert len(can_update_history_events) == 4 + portfolio_4 = loaded_db.get(Portfolio, 4) + portfolio_1 = loaded_db.get(Portfolio, 1) + can_portfolio_event = can_update_history_events[2] + can_division_event = can_update_history_events[3] + assert can_portfolio_event.history_title == "CAN Portfolio Edited" + assert ( + can_portfolio_event.history_message + == f"Steve Tekell changed the portfolio from {portfolio_4.name} to {portfolio_1.name}" + ) + assert can_portfolio_event.history_type == CANHistoryType.CAN_PORTFOLIO_EDITED + + assert can_division_event.history_title == "CAN Division Edited" + assert ( + can_division_event.history_message + == f"Steve Tekell changed the division from {portfolio_4.division.name} to {portfolio_1.division.name}" + ) + assert can_division_event.history_type == CANHistoryType.CAN_DIVISION_EDITED + + +@pytest.mark.usefixtures("app_ctx") +def test_update_can_portfolio_can_history_system_user(loaded_db): + update_can_event = loaded_db.get(OpsEvent, 29) + can_history_trigger(update_can_event, loaded_db) + can_update_history_events = ( + loaded_db.execute(select(CANHistory).where(CANHistory.ops_event_id == 29)).scalars().all() + ) + assert len(can_update_history_events) == 4 portfolio_1 = loaded_db.get(Portfolio, 1) portfolio_6 = loaded_db.get(Portfolio, 6) - nickname_can_history_event = can_update_history_events[0] - assert nickname_can_history_event.history_title == "CAN Portfolio Edited" + can_portfolio_event = can_update_history_events[2] + can_division_event = can_update_history_events[3] + assert can_portfolio_event.history_title == "CAN Portfolio Edited" assert ( - nickname_can_history_event.history_message + can_portfolio_event.history_message == f"CAN portfolio changed from {portfolio_1.name} to {portfolio_6.name} during FY 2025 data import" ) - assert nickname_can_history_event.history_type == CANHistoryType.CAN_PORTFOLIO_EDITED + assert can_portfolio_event.history_type == CANHistoryType.CAN_PORTFOLIO_EDITED + + assert can_division_event.history_title == "CAN Division Edited" + assert ( + can_division_event.history_message + == f"CAN division changed from {portfolio_1.division.name} to {portfolio_6.division.name} during FY 2025 data import" + ) + assert can_division_event.history_type == CANHistoryType.CAN_DIVISION_EDITED + + +@pytest.mark.usefixtures("app_ctx") +def test_update_can_nickname_system_user(loaded_db): + update_can_event = loaded_db.get(OpsEvent, 30) + can_history_trigger(update_can_event, loaded_db) + can_update_history_events = ( + loaded_db.execute(select(CANHistory).where(CANHistory.ops_event_id == 30)).scalars().all() + ) + assert len(can_update_history_events) == 2 + + nickname_event = can_update_history_events[1] + + assert nickname_event.history_title == "Nickname Edited" + assert ( + nickname_event.history_message + == "Nickname changed from Interagency Agreements to IAA-Incoming during FY 2025 data import" + ) + assert nickname_event.history_type == CANHistoryType.CAN_NICKNAME_EDITED