diff --git a/epictrack-api/migrations/versions/74531216e674_columns_moves_to_work.py b/epictrack-api/migrations/versions/74531216e674_columns_moves_to_work.py new file mode 100644 index 000000000..b5a1dec88 --- /dev/null +++ b/epictrack-api/migrations/versions/74531216e674_columns_moves_to_work.py @@ -0,0 +1,96 @@ +"""columns moves to work + +Revision ID: 74531216e674 +Revises: 73c6150268e5 +Create Date: 2023-11-10 10:19:54.637944 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '74531216e674' +down_revision = '73c6150268e5' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('work_issue_updates', schema=None) as batch_op: + batch_op.add_column(sa.Column('is_approved', sa.Boolean(), nullable=False, server_default='false')) + batch_op.add_column(sa.Column('approved_by', sa.String(length=255), nullable=True)) + + with op.batch_alter_table('work_issue_updates_history', schema=None) as batch_op: + batch_op.add_column(sa.Column('is_approved', sa.Boolean(), autoincrement=False, nullable=False, server_default='false')) + batch_op.add_column(sa.Column('approved_by', sa.String(length=255), autoincrement=False, nullable=True)) + + with op.batch_alter_table('work_issues', schema=None) as batch_op: + batch_op.drop_column('is_approved') + batch_op.drop_column('approved_by') + + with op.batch_alter_table('work_issues_history', schema=None) as batch_op: + batch_op.drop_column('is_approved') + batch_op.drop_column('approved_by') + + with op.batch_alter_table('work_statuses', schema=None) as batch_op: + batch_op.drop_column('notes') + + with op.batch_alter_table('work_statuses_history', schema=None) as batch_op: + batch_op.alter_column('posted_date', + existing_type=sa.DATE(), + type_=sa.DateTime(timezone=True), + existing_nullable=False, + autoincrement=False) + batch_op.drop_column('notes') + + with op.batch_alter_table('works', schema=None) as batch_op: + batch_op.add_column(sa.Column('status_notes', sa.Text(), nullable=True)) + batch_op.add_column(sa.Column('issue_notes', sa.Text(), nullable=True)) + + with op.batch_alter_table('works_history', schema=None) as batch_op: + batch_op.add_column(sa.Column('status_notes', sa.Text(), autoincrement=False, nullable=True)) + batch_op.add_column(sa.Column('issue_notes', sa.Text(), autoincrement=False, nullable=True)) + + # ### 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.drop_column('issue_notes') + batch_op.drop_column('status_notes') + + with op.batch_alter_table('works', schema=None) as batch_op: + batch_op.drop_column('issue_notes') + batch_op.drop_column('status_notes') + + with op.batch_alter_table('work_statuses_history', schema=None) as batch_op: + batch_op.add_column(sa.Column('notes', sa.TEXT(), autoincrement=False, nullable=True)) + batch_op.alter_column('posted_date', + existing_type=sa.DateTime(timezone=True), + type_=sa.DATE(), + existing_nullable=False, + autoincrement=False) + + with op.batch_alter_table('work_statuses', schema=None) as batch_op: + batch_op.add_column(sa.Column('notes', sa.TEXT(), autoincrement=False, nullable=True)) + + with op.batch_alter_table('work_issues_history', schema=None) as batch_op: + batch_op.add_column(sa.Column('approved_by', sa.VARCHAR(length=255), autoincrement=False, nullable=True)) + batch_op.add_column(sa.Column('is_approved', sa.BOOLEAN(), autoincrement=False, nullable=False, server_default='false')) + + with op.batch_alter_table('work_issues', schema=None) as batch_op: + batch_op.add_column(sa.Column('approved_by', sa.VARCHAR(length=255), autoincrement=False, nullable=True)) + batch_op.add_column(sa.Column('is_approved', sa.BOOLEAN(), autoincrement=False, nullable=False, server_default='false')) + + with op.batch_alter_table('work_issue_updates_history', schema=None) as batch_op: + batch_op.drop_column('approved_by') + batch_op.drop_column('is_approved') + + with op.batch_alter_table('work_issue_updates', schema=None) as batch_op: + batch_op.drop_column('approved_by') + batch_op.drop_column('is_approved') + + # ### end Alembic commands ### diff --git a/epictrack-api/requirements.txt b/epictrack-api/requirements.txt index bcd87346d..06db19b95 100644 --- a/epictrack-api/requirements.txt +++ b/epictrack-api/requirements.txt @@ -31,7 +31,7 @@ greenlet==3.0.1 gunicorn==21.2.0 idna==3.4 importlib-metadata==6.8.0 -importlib-resources==6.1.0 +importlib-resources==6.1.1 itsdangerous==2.1.2 jsonschema-specifications==2023.7.1 jsonschema==4.19.2 @@ -52,7 +52,7 @@ pytz==2023.3.post1 referencing==0.30.2 reportlab==3.6.12 requests==2.31.0 -rpds-py==0.10.6 +rpds-py==0.12.0 rsa==4.9 six==1.16.0 typing_extensions==4.8.0 diff --git a/epictrack-api/src/api/models/work.py b/epictrack-api/src/api/models/work.py index 0e9679b79..e120ee756 100644 --- a/epictrack-api/src/api/models/work.py +++ b/epictrack-api/src/api/models/work.py @@ -49,6 +49,8 @@ class Work(BaseModelVersioned): project_tracking_number = Column(String(255), nullable=True, default=None) work_tracking_number = Column(String(255), nullable=True, default=None) first_nation_notes = Column(String) + status_notes = Column(Text) + issue_notes = Column(Text) start_date = Column(DateTime(timezone=True)) anticipated_decision_date = Column(DateTime(timezone=True)) diff --git a/epictrack-api/src/api/models/work_issue_updates.py b/epictrack-api/src/api/models/work_issue_updates.py index 6c0d83891..53e92e86d 100644 --- a/epictrack-api/src/api/models/work_issue_updates.py +++ b/epictrack-api/src/api/models/work_issue_updates.py @@ -13,7 +13,7 @@ # limitations under the License. """Model to handle all operations related to Issues.""" -from sqlalchemy import Column, ForeignKey, Integer, String +from sqlalchemy import Column, ForeignKey, Integer, String, Boolean from sqlalchemy.orm import relationship from .base_model import BaseModelVersioned @@ -27,6 +27,9 @@ class WorkIssueUpdates(BaseModelVersioned): id = Column(Integer, primary_key=True, autoincrement=True) description = Column(String(2000), nullable=False) + is_approved = Column(Boolean(), default=False, nullable=True) + approved_by = Column(String(255), default=None, nullable=True) + work_issue_id = Column(ForeignKey('work_issues.id'), nullable=False) work_issue = relationship('WorkIssues', back_populates='updates') diff --git a/epictrack-api/src/api/models/work_issues.py b/epictrack-api/src/api/models/work_issues.py index 9ac0bc75a..b159c8b7e 100644 --- a/epictrack-api/src/api/models/work_issues.py +++ b/epictrack-api/src/api/models/work_issues.py @@ -33,8 +33,6 @@ class WorkIssues(BaseModelVersioned): is_high_priority = Column(Boolean(), default=False, nullable=False) start_date = Column(DateTime(timezone=True), nullable=False) expected_resolution_date = Column(DateTime(timezone=True), nullable=True) - is_approved = Column(Boolean(), default=False, nullable=False) - approved_by = Column(String(255), default=None, nullable=True) work_id = Column(ForeignKey('works.id'), nullable=False) work = relationship('Work', foreign_keys=[work_id], lazy='select') diff --git a/epictrack-api/src/api/models/work_status.py b/epictrack-api/src/api/models/work_status.py index 80cdc68fd..bc56495eb 100644 --- a/epictrack-api/src/api/models/work_status.py +++ b/epictrack-api/src/api/models/work_status.py @@ -16,7 +16,7 @@ from typing import List -from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Text, DateTime, desc +from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, DateTime, desc from sqlalchemy.orm import relationship from .base_model import BaseModelVersioned @@ -29,7 +29,6 @@ class WorkStatus(BaseModelVersioned): id = Column(Integer, primary_key=True, autoincrement=True) description = Column(String(2000), nullable=False) - notes = Column(Text) posted_date = Column(DateTime(timezone=True), nullable=False) posted_by = Column(String(100), nullable=True) work_id = Column(ForeignKey('works.id'), nullable=False) diff --git a/epictrack-api/src/api/resources/work.py b/epictrack-api/src/api/resources/work.py index ffd92eef6..33b86b4e8 100644 --- a/epictrack-api/src/api/resources/work.py +++ b/epictrack-api/src/api/resources/work.py @@ -25,7 +25,6 @@ from api.utils import auth, profiletime from api.utils.util import cors_preflight - API = Namespace("works", description="Works") @@ -317,6 +316,23 @@ def patch(work_id): return res.WorkResourceResponseSchema().dump(work), HTTPStatus.OK +@cors_preflight("PATCH") +@API.route("//notes", methods=["PATCH", "OPTIONS"]) +class WorkNotes(Resource): + """Endpoints to handle work related notes""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def patch(work_id): + """Save the notes to corresponding work""" + req.WorkIdPathParameterSchema().load(request.view_args) + notes = req.WorkNotesBodySchema().load(API.payload) + work = WorkService.save_notes(work_id, notes) + return res.WorkResourceResponseSchema().dump(work), HTTPStatus.OK + + @cors_preflight("GET, PUT") @API.route("/first-nations/", methods=["GET", "PUT", "OPTIONS"]) class WorkFirstNation(Resource): diff --git a/epictrack-api/src/api/resources/work_issues.py b/epictrack-api/src/api/resources/work_issues.py index d772c1d69..f792208bc 100644 --- a/epictrack-api/src/api/resources/work_issues.py +++ b/epictrack-api/src/api/resources/work_issues.py @@ -66,7 +66,7 @@ def put(work_id, issue_id): return res.WorkIssuesResponseSchema().dump(work_issues), HTTPStatus.CREATED -@API.route("//issue_update", methods=["POST"]) +@API.route("//update", methods=["POST"]) class WorkIssueUpdate(Resource): """Endpoint resource to manage updates for a specific issue.""" @@ -84,7 +84,7 @@ def post(work_id, issue_id): @cors_preflight("PATCH") -@API.route("//approve", methods=["PATCH", "OPTIONS"]) +@API.route("//update//approve", methods=["PATCH", "OPTIONS"]) class ApproveIssues(Resource): """Endpoint resource to manage approving of work status.""" @@ -92,12 +92,9 @@ class ApproveIssues(Resource): @cors.crossdomain(origin="*") @auth.require @profiletime - def patch(work_id, issue_id): + # pylint: disable=unused-argument + def patch(work_id, issue_id, update_id): """Approve a work status.""" - existing_work_issues = WorkIssuesService.find_work_issue_by_id(work_id, issue_id) - if existing_work_issues is None: - return {"message": "Work issues not found"}, HTTPStatus.NOT_FOUND + approved_work_issues = WorkIssuesService.approve_work_issues(issue_id, update_id) - approved_work_issues = WorkIssuesService.approve_work_issues(existing_work_issues) - - return res.WorkIssuesResponseSchema().dump(approved_work_issues), HTTPStatus.OK + return res.WorkIssueUpdatesResponseSchema().dump(approved_work_issues), HTTPStatus.OK diff --git a/epictrack-api/src/api/resources/work_status.py b/epictrack-api/src/api/resources/work_status.py index 8e17b9de1..3305b3c82 100644 --- a/epictrack-api/src/api/resources/work_status.py +++ b/epictrack-api/src/api/resources/work_status.py @@ -89,19 +89,3 @@ def patch(work_id, status_id): approved_work_status = WorkStatusService.approve_work_status(existing_work_status) return res.WorkStatusResponseSchema().dump(approved_work_status), HTTPStatus.OK - - -@cors_preflight("PATCH") -@API.route("//notes", methods=["PATCH", "OPTIONS"]) -class StatusNotes(Resource): - """Endpoints to handle status notes""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def patch(work_id, status_id): - """Save the notes to corresponding work""" - notes = req.WorkStatusNotesBodySchema().load(API.payload)["notes"] - work_status = WorkStatusService.save_notes(work_id, status_id, notes) - return res.WorkStatusResponseSchema().dump(work_status), HTTPStatus.OK diff --git a/epictrack-api/src/api/schemas/request/__init__.py b/epictrack-api/src/api/schemas/request/__init__.py index e01c1fb75..7eb4c15b9 100644 --- a/epictrack-api/src/api/schemas/request/__init__.py +++ b/epictrack-api/src/api/schemas/request/__init__.py @@ -49,5 +49,5 @@ WorkBodyParameterSchema, WorkExistenceQueryParamSchema, WorkFirstNationImportBodyParamSchema, WorkFirstNationNotesBodySchema, WorkIdPathParameterSchema, WorkIdPhaseIdPathParameterSchema, WorkPlanDownloadQueryParamSchema, WorkTypeIdQueryParamSchema, WorkStatusParameterSchema, - WorkIssuesParameterSchema, WorkIssuesUpdateSchema, WorkStatusNotesBodySchema) + WorkIssuesParameterSchema, WorkIssuesUpdateSchema, WorkNotesBodySchema) from .act_section_request import ActSectionQueryParameterSchema diff --git a/epictrack-api/src/api/schemas/request/work_request.py b/epictrack-api/src/api/schemas/request/work_request.py index 2798e173b..716a63005 100644 --- a/epictrack-api/src/api/schemas/request/work_request.py +++ b/epictrack-api/src/api/schemas/request/work_request.py @@ -267,11 +267,17 @@ class WorkIssuesUpdateSchema(WorkIssuesParameterSchema): ) -class WorkStatusNotesBodySchema(RequestBodyParameterSchema): - """Work status notes body parameter schema""" +class WorkNotesBodySchema(RequestBodyParameterSchema): + """Work notes body parameter schema""" notes = fields.Str( metadata={"description": "Work status notes"}, validate=validate.Length(min=1), required=True, ) + + note_type = fields.Str( + metadata={"description": "Type of work status notes"}, + validate=validate.OneOf(['status_notes', 'issue_notes']), # Add your predefined types + required=True, + ) diff --git a/epictrack-api/src/api/schemas/response/work_response.py b/epictrack-api/src/api/schemas/response/work_response.py index 9e8f64ccf..7f8da7a70 100644 --- a/epictrack-api/src/api/schemas/response/work_response.py +++ b/epictrack-api/src/api/schemas/response/work_response.py @@ -118,6 +118,11 @@ class Meta( responsible_epd = fields.Nested(StaffSchema, exclude=("position",), dump_only=True) work_lead = fields.Nested(StaffSchema, exclude=("position",), dump_only=True) staff = fields.Nested(WorkStaffRoleReponseSchema(many=True), dump_default=[]) + work_state = fields.Method("get_work_state") + + def get_work_state(self, obj: Work) -> str: + """Return the work state""" + return obj.work_state.value if obj.work_state else None class WorkPhaseAdditionalInfoResponseSchema(Schema): diff --git a/epictrack-api/src/api/services/work.py b/epictrack-api/src/api/services/work.py index be586e1d4..70d166870 100644 --- a/epictrack-api/src/api/services/work.py +++ b/epictrack-api/src/api/services/work.py @@ -428,6 +428,29 @@ def save_first_nation_notes(cls, work_id: int, notes: str) -> Work: work.save() return work + @classmethod + def save_notes(cls, work_id: int, notes_payload: dict) -> Work: + """Save notes to the given column in the work.""" + # if column name cant map the type in the UI , add it here.. + note_type_mapping = { + 'first_nation': 'first_nation_notes', + } + + work = cls.find_by_id(work_id) + notes = notes_payload.get("notes") + note_type = notes_payload.get("note_type") + + if hasattr(work, note_type): + setattr(work, note_type, notes) + else: + mapped_column = note_type_mapping.get(note_type) + if mapped_column is None: + raise ResourceExistsError(f"No work note type {note_type} nation association found") + setattr(work, mapped_column, notes) + + work.save() + return work + @classmethod def find_work_first_nation(cls, work_nation_id: int) -> IndigenousWork: """Find work indigenous nation by id""" diff --git a/epictrack-api/src/api/services/work_issues.py b/epictrack-api/src/api/services/work_issues.py index 6921edf0b..42c11caa3 100644 --- a/epictrack-api/src/api/services/work_issues.py +++ b/epictrack-api/src/api/services/work_issues.py @@ -14,8 +14,8 @@ """Service to manage Work status.""" from typing import Dict, List -from api.models import WorkIssueUpdates as WorkIssueUpdatesModel from api.exceptions import ResourceNotFoundError +from api.models import WorkIssueUpdates as WorkIssueUpdatesModel from api.models import WorkIssues as WorkIssuesModel from api.utils import TokenInfo @@ -71,17 +71,20 @@ def add_work_issue_update(cls, work_id, issue_id, description_data: List[str]): return WorkIssuesModel.find_by_id(issue_id) @classmethod - def approve_work_issues(cls, work_issues): + def approve_work_issues(cls, issue_id, update_id): """Approve a work status.""" - if work_issues.is_approved: - return work_issues + results = WorkIssueUpdatesModel.find_by_params({"id": update_id, "work_issue_id": issue_id}) + if not results: + raise ResourceNotFoundError("Work issue Description doesnt exist") - work_issues.is_approved = True - work_issues.approved_by = TokenInfo.get_username() + work_issue_update: WorkIssueUpdatesModel = results[0] - work_issues.save() + work_issue_update.is_approved = True + work_issue_update.approved_by = TokenInfo.get_username() - return work_issues + work_issue_update.save() + + return work_issue_update @classmethod def edit_issue_update(cls, work_id, issue_id, issue_data): diff --git a/epictrack-api/src/api/services/work_status.py b/epictrack-api/src/api/services/work_status.py index e62986783..4aa954456 100644 --- a/epictrack-api/src/api/services/work_status.py +++ b/epictrack-api/src/api/services/work_status.py @@ -14,7 +14,7 @@ """Service to manage Work status.""" from http import HTTPStatus from typing import Dict -from api.exceptions import ResourceNotFoundError + from api.exceptions import BusinessError from api.models import WorkStatus as WorkStatusModel from api.utils import TokenInfo @@ -62,17 +62,6 @@ def update_work_status(cls, work_status: WorkStatusModel, work_status_data: dict return work_status - @classmethod - def save_notes(cls, work_id, status_id, notes: str) -> WorkStatusModel: - """Save a work status note.""" - work_status: WorkStatusModel = WorkStatusService.find_work_status_by_id(work_id, status_id) - if not work_status: - raise ResourceNotFoundError("Work status not found") - work_status.notes = notes - work_status.save() - - return work_status - @classmethod def approve_work_status(cls, work_status): """Approve a work status."""