diff --git a/epictrack-api/migrations/versions/5b390f888c3f_simple_title_in_work_history.py b/epictrack-api/migrations/versions/5b390f888c3f_simple_title_in_work_history.py new file mode 100644 index 000000000..7041386d5 --- /dev/null +++ b/epictrack-api/migrations/versions/5b390f888c3f_simple_title_in_work_history.py @@ -0,0 +1,62 @@ +"""simple_title_in_work_history + +Revision ID: 5b390f888c3f +Revises: 73868272c32c +Create Date: 2024-02-06 22:38:55.052178 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "5b390f888c3f" +down_revision = "73868272c32c" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + + with op.batch_alter_table("indigenous_works_history", schema=None) as batch_op: + batch_op.alter_column( + "indigenous_consultation_level_id", + existing_type=sa.INTEGER(), + nullable=False, + autoincrement=False, + ) + + with op.batch_alter_table("works_history", schema=None) as batch_op: + batch_op.alter_column( + "simple_title", + existing_type=sa.TEXT(), + type_=sa.String(), + existing_nullable=True, + autoincrement=False, + ) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("works_history", schema=None) as batch_op: + batch_op.alter_column( + "simple_title", + existing_type=sa.String(), + type_=sa.TEXT(), + existing_nullable=True, + autoincrement=False, + ) + + with op.batch_alter_table("indigenous_works_history", schema=None) as batch_op: + batch_op.alter_column( + "indigenous_consultation_level_id", + existing_type=sa.INTEGER(), + nullable=True, + autoincrement=False, + ) + + # ### end Alembic commands ### diff --git a/epictrack-api/src/api/application_constants/__init__.py b/epictrack-api/src/api/application_constants/__init__.py new file mode 100644 index 000000000..f789078ac --- /dev/null +++ b/epictrack-api/src/api/application_constants/__init__.py @@ -0,0 +1,16 @@ +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This module holds the application wide constants""" + +MIN_WORK_START_DATE = "1995-06-30" diff --git a/epictrack-api/src/api/reports/resource_forecast_report.py b/epictrack-api/src/api/reports/resource_forecast_report.py index 00cd8067d..da06551d3 100644 --- a/epictrack-api/src/api/reports/resource_forecast_report.py +++ b/epictrack-api/src/api/reports/resource_forecast_report.py @@ -225,7 +225,7 @@ def _fetch_data(self, report_date: datetime): WorkType.name.label("ea_type"), WorkType.report_title.label("ea_type_label"), WorkType.sort_order.label("ea_type_sort_order"), - PhaseCode.name.label("project_phase"), + WorkPhase.name.label("project_phase"), EAAct.name.label("ea_act"), FederalInvolvement.name.label("iaac"), SubType.short_name.label("sub_type"), diff --git a/epictrack-api/src/api/services/event.py b/epictrack-api/src/api/services/event.py index 7c8f1717e..b5520465f 100644 --- a/epictrack-api/src/api/services/event.py +++ b/epictrack-api/src/api/services/event.py @@ -16,6 +16,7 @@ import functools from datetime import datetime, timedelta from typing import List +import pytz from sqlalchemy import and_, extract, func, or_ @@ -42,6 +43,7 @@ from api.models.work_type import WorkType from api.services.outcome_configuration import OutcomeConfigurationService from api.utils import util +from api.application_constants import MIN_WORK_START_DATE from ..utils.roles import Membership from ..utils.roles import Role as KeycloakRole @@ -295,6 +297,7 @@ def _process_events( current_work_phase_index = util.find_index_in_array( all_work_phases, current_work_phase ) + cls._validate_dates(event, current_work_phase, all_work_phases) cls._previous_event_acutal_date_rule( all_work_events, all_work_phases, current_work_phase_index, event, event_old ) @@ -383,6 +386,88 @@ def _process_events( current_event_index, ) + @classmethod + def _validate_dates( + cls, event: Event, current_work_phase: WorkPhase, all_work_phases: [WorkPhase] + ): + """Perform date validations for the min and max dates for events""" + if event.actual_date: + actual_min_date = cls._find_actual_date_min( + event, current_work_phase, all_work_phases + ) + actual_max_date = cls._find_actual_date_max(current_work_phase) + if ( + event.actual_date < actual_min_date + or event.actual_date > actual_max_date + ): + raise UnprocessableEntityError( + f"Actual date should be between {actual_min_date} and {actual_max_date}" + ) + if not event.actual_date: + anticipated_min_date = cls._find_anticipated_date_min( + event, current_work_phase, all_work_phases + ) + if event.anticipated_date < anticipated_min_date: + raise UnprocessableEntityError( + f"Anticipdated date should be greater than {anticipated_min_date}" + ) + + @classmethod + def _find_anticipated_date_min( + cls, event: Event, current_work_phase: WorkPhase, all_work_phases: [WorkPhase] + ): + """Return the min date of anticipated date""" + anticipated_date_min = ( + datetime.strptime(MIN_WORK_START_DATE, "%Y-%m-%d").replace(tzinfo=pytz.utc) + if cls._is_start_event(event) + and cls._is_start_phase(current_work_phase, all_work_phases) + else current_work_phase.work.start_date + ) + return anticipated_date_min + + @classmethod + def _find_actual_date_min( + cls, event: Event, current_work_phase: WorkPhase, all_work_phases: [WorkPhase] + ): + """Return the min date of actual date""" + actual_date_min = ( + datetime.strptime(MIN_WORK_START_DATE, "%Y-%m-%d").replace(tzinfo=pytz.utc) + if cls._is_start_event(event) + and cls._is_start_phase(current_work_phase, all_work_phases) + else current_work_phase.start_date + ) + return actual_date_min + + @classmethod + def _find_actual_date_max(cls, current_work_phase: WorkPhase): + """Return the max date of actual date""" + date_diff_days = ( + (current_work_phase.end_date - current_work_phase.start_date).days + if current_work_phase.legislated + else 0 + ) + actual_date_max = ( + current_work_phase.start_date + timedelta(days=date_diff_days) + if current_work_phase.legislated + else datetime.utcnow().replace(tzinfo=pytz.utc) + ) + return actual_date_max + + @classmethod + def _is_start_event(cls, event): + """Return true if the given event is start event""" + return ( + event.event_configuration.event_position.value + == EventPositionEnum.START.value + ) + + @classmethod + def _is_start_phase( + cls, current_work_phase: WorkPhase, all_work_phases: [WorkPhase] + ): + """Return true if the current phase is start phase""" + return all_work_phases[0].id == current_work_phase.id + @classmethod def _handle_work_phase_for_start_event( cls,