Skip to content

Commit

Permalink
Merge pull request #2831 from HHS/OPS-2781/adding-remaining-cans-endp…
Browse files Browse the repository at this point in the history
…oints

Adding /cans/ endpoints and service layer refactor
  • Loading branch information
rajohnson90 authored Sep 23, 2024
2 parents 26e518c + d08f92b commit 9fdcd34
Show file tree
Hide file tree
Showing 10 changed files with 683 additions and 48 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Adding create, update, and delete events for CANs
Revision ID: cbbaf27a11ee
Revises: eb4261420779
Create Date: 2024-09-19 18:35:25.734075+00:00
"""
from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op
from alembic_postgresql_enum import TableReference

# revision identifiers, used by Alembic.
revision: str = 'cbbaf27a11ee'
down_revision: Union[str, None] = 'eb4261420779'
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.sync_enum_values('ops', 'opseventtype', ['CREATE_BLI', 'UPDATE_BLI', 'DELETE_BLI', 'SEND_BLI_FOR_APPROVAL', 'CREATE_PROJECT', 'CREATE_NEW_AGREEMENT', 'UPDATE_AGREEMENT', 'DELETE_AGREEMENT', 'CREATE_NEW_CAN', 'UPDATE_CAN', 'DELETE_CAN', 'ACKNOWLEDGE_NOTIFICATION', 'CREATE_BLI_PACKAGE', 'UPDATE_BLI_PACKAGE', 'CREATE_SERVICES_COMPONENT', 'UPDATE_SERVICES_COMPONENT', 'DELETE_SERVICES_COMPONENT', 'CREATE_PROCUREMENT_ACQUISITION_PLANNING', 'UPDATE_PROCUREMENT_ACQUISITION_PLANNING', 'DELETE_PROCUREMENT_ACQUISITION_PLANNING', 'CREATE_DOCUMENT', 'UPDATE_DOCUMENT', 'LOGIN_ATTEMPT', 'LOGOUT', 'GET_USER_DETAILS', 'CREATE_USER', 'UPDATE_USER', 'DEACTIVATE_USER'],
[TableReference(table_schema='ops', table_name='ops_event', column_name='event_type'), TableReference(table_schema='ops', table_name='ops_event_version', column_name='event_type')],
enum_values_to_rename=[])
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.sync_enum_values('ops', 'opseventtype', ['CREATE_BLI', 'UPDATE_BLI', 'DELETE_BLI', 'SEND_BLI_FOR_APPROVAL', 'CREATE_PROJECT', 'CREATE_NEW_AGREEMENT', 'UPDATE_AGREEMENT', 'DELETE_AGREEMENT', 'ACKNOWLEDGE_NOTIFICATION', 'CREATE_BLI_PACKAGE', 'UPDATE_BLI_PACKAGE', 'CREATE_SERVICES_COMPONENT', 'UPDATE_SERVICES_COMPONENT', 'DELETE_SERVICES_COMPONENT', 'CREATE_PROCUREMENT_ACQUISITION_PLANNING', 'UPDATE_PROCUREMENT_ACQUISITION_PLANNING', 'DELETE_PROCUREMENT_ACQUISITION_PLANNING', 'CREATE_DOCUMENT', 'UPDATE_DOCUMENT', 'LOGIN_ATTEMPT', 'LOGOUT', 'GET_USER_DETAILS', 'CREATE_USER', 'UPDATE_USER', 'DEACTIVATE_USER'],
[TableReference(table_schema='ops', table_name='ops_event', column_name='event_type'), TableReference(table_schema='ops', table_name='ops_event_version', column_name='event_type')],
enum_values_to_rename=[])
# ### end Alembic commands ###
5 changes: 5 additions & 0 deletions backend/data_tools/data/user_data.json5
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@
"POST_BLI_PACKAGE",

"GET_CAN",
"POST_CAN",
"PATCH_CAN",
"PUT_CAN",
"DELETE_CAN",

"GET_DIVISION",

Expand Down Expand Up @@ -254,6 +258,7 @@
"PUT_CAN",
"PATCH_CAN",
"POST_CAN",
"DELETE_CAN",

"GET_DIVISION",

Expand Down
5 changes: 5 additions & 0 deletions backend/models/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ class OpsEventType(Enum):
UPDATE_AGREEMENT = auto()
DELETE_AGREEMENT = auto()

# CAN Related Events
CREATE_NEW_CAN = auto()
UPDATE_CAN = auto()
DELETE_CAN = auto()

# Notification Related Events
ACKNOWLEDGE_NOTIFICATION = auto()

Expand Down
47 changes: 47 additions & 0 deletions backend/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,31 @@ paths:
responses:
"200":
description: OK
post:
tags:
- CANs
operationId: createCAN
summary: Create a new CAN object
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateCANRequestSchema"
examples:
"0":
$ref: "#/components/examples/CreateCANRequestSchema"
responses:
"201":
description: Created
content:
application/json:
schema:
$ref: "#/components/schemas/CAN"
"400":
description: Bad Request
"401":
description: Insufficient Privileges to use this endpoint.
/api/v1/cans/{can_id}:
get:
tags:
Expand Down Expand Up @@ -2033,6 +2058,20 @@ components:
type: string
id:
type: integer
CreateCANRequestSchema:
description: The request object for creating a new Common Accounting Number (CAN) object.
properties:
nick_name:
type: string
number:
type: string
description:
type: string
portfolio_id:
type: integer
required:
- number
- portfolio_id
CAN:
description: Common Accounting Number (CAN) Object
type: object
Expand Down Expand Up @@ -3584,6 +3623,14 @@ components:
"updated_by": 1
}
]
CreateCanRequestSchema:
value: |
{
nick_name: "Very Good CAN",
number: "G998235",
portfolio_id: 6,
description: "A very good CAN to use for examples."
}
Notifications:
value: |
[
Expand Down
107 changes: 69 additions & 38 deletions backend/ops_api/ops/resources/cans.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
from dataclasses import dataclass
from typing import List, Optional, cast
from typing import List, Optional

import desert
from flask import Response, current_app, request
from flask_jwt_extended import jwt_required
from sqlalchemy import select
from sqlalchemy.orm import InstrumentedAttribute

from models import OpsEventType
from models.base import BaseModel
from models.cans import CAN
from ops_api.ops.auth.auth_types import Permission, PermissionType
from ops_api.ops.auth.decorators import is_authorized
from ops_api.ops.base_views import BaseItemAPI, BaseListAPI
from ops_api.ops.schemas.cans import CANSchema
from ops_api.ops.schemas.cans import CANSchema, CreateUpdateCANRequestSchema, GetCANListRequestSchema
from ops_api.ops.services.cans import CANService
from ops_api.ops.utils.errors import error_simulator
from ops_api.ops.utils.query_helpers import QueryHelper
from ops_api.ops.utils.events import OpsEventHandler
from ops_api.ops.utils.response import make_response_with_headers


Expand All @@ -26,57 +27,87 @@ class ListAPIRequest:
class CANItemAPI(BaseItemAPI):
def __init__(self, model):
super().__init__(model)
self.can_service = CANService()

@is_authorized(PermissionType.GET, Permission.CAN)
def get(self, id: int) -> Response:
schema = CANSchema()
item = self._get_item(id)

if item:
response = make_response_with_headers(schema.dump(item))
else:
response = make_response_with_headers({}, 404)

return response
item = self.can_service.get(id)
return make_response_with_headers(schema.dump(item))

@is_authorized(PermissionType.PATCH, Permission.CAN)
def patch(self, id: int) -> Response:
"""
Update a CAN with only the fields provided in the request body.
"""
with OpsEventHandler(OpsEventType.UPDATE_CAN) as meta:
request_data = request.get_json()
# Setting partial to true ignores any missing fields.
schema = CreateUpdateCANRequestSchema(partial=True)
serialized_request = schema.load(request_data)

updated_can = self.can_service.update(serialized_request, id)
serialized_can = schema.dump(updated_can)
meta.metadata.update({"updated_can": serialized_can})
return make_response_with_headers(schema.dump(updated_can))

@is_authorized(PermissionType.PATCH, Permission.CAN)
def put(self, id: int) -> Response:
"""
Update a CAN with only the fields provided in the request body.
"""
with OpsEventHandler(OpsEventType.UPDATE_CAN) as meta:
request_data = request.get_json()
# Setting partial to true ignores any missing fields.
schema = CreateUpdateCANRequestSchema()
serialized_request = schema.load(request_data)

updated_can = self.can_service.update(serialized_request, id)
serialized_can = schema.dump(updated_can)
meta.metadata.update({"updated_can": serialized_can})
return make_response_with_headers(schema.dump(updated_can))

@is_authorized(PermissionType.DELETE, Permission.CAN)
def delete(self, id: int) -> Response:
"""
Delete a CAN with given id."""
with OpsEventHandler(OpsEventType.DELETE_CAN) as meta:
self.can_service.delete(id)
meta.metadata.update({"Deleted BudgetLineItem": id})
return make_response_with_headers({"message": "CAN deleted", "id": id}, 200)


class CANListAPI(BaseListAPI):
def __init__(self, model):
super().__init__(model)
self.can_service = CANService()
self._get_input_schema = desert.schema(ListAPIRequest)

@staticmethod
def _get_query(search=None):
stmt = select(CAN).order_by(CAN.id)

query_helper = QueryHelper(stmt)

if search is not None and len(search) == 0:
query_helper.return_none()
elif search:
query_helper.add_search(cast(InstrumentedAttribute, CAN.number), search)

stmt = query_helper.get_stmt()
current_app.logger.debug(f"SQL: {stmt}")

return stmt

@jwt_required()
@error_simulator
def get(self) -> Response:
errors = self._get_input_schema.validate(request.args)
list_schema = GetCANListRequestSchema()
get_request = list_schema.load(request.args)
result = self.can_service.get_list(**get_request)
can_schema = CANSchema()
return make_response_with_headers([can_schema.dump(can) for can in result])

if errors:
return make_response_with_headers(errors, 400)

request_data: ListAPIRequest = self._get_input_schema.load(request.args)
stmt = self._get_query(request_data.search)
result = current_app.db_session.execute(stmt).all()
return make_response_with_headers([can_schema.dump(i) for item in result for i in item])

@is_authorized(PermissionType.POST, Permission.CAN)
def post(self) -> Response:
return "Hello"
"""
Create a new Common Accounting Number (CAN) object.
"""
with OpsEventHandler(OpsEventType.CREATE_NEW_CAN) as meta:
request_data = request.get_json()
schema = CreateUpdateCANRequestSchema()
serialized_request = schema.load(request_data)

created_can = self.can_service.create(serialized_request)

can_schema = CANSchema()
serialized_can = can_schema.dump(created_can)
meta.metadata.update({"new_can": serialized_can})
return make_response_with_headers(serialized_can, 201)


class CANsByPortfolioAPI(BaseItemAPI):
Expand Down
21 changes: 17 additions & 4 deletions backend/ops_api/ops/schemas/cans.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
from marshmallow import Schema, fields

from models import PortfolioStatus
from models import CANMethodOfTransfer, PortfolioStatus
from ops_api.ops.schemas.budget_line_items import BudgetLineItemResponseSchema
from ops_api.ops.schemas.projects import ProjectSchema
from ops_api.ops.schemas.users import SafeUserSchema


class GetCANListRequestSchema(Schema):
search = fields.String(allow_none=True)


class CreateUpdateCANRequestSchema(Schema):
nick_name = fields.String(load_default=None)
number = fields.String(required=True)
description = fields.String(allow_none=True, load_default=None)
portfolio_id = fields.Integer(required=True)
funding_details_id = fields.Integer(allow_none=True, load_default=None)


class BasicCANSchema(Schema):
active_period = fields.Integer(allow_none=True)
display_name = fields.String(allow_none=True)
Expand All @@ -14,6 +26,7 @@ class BasicCANSchema(Schema):
description = fields.String(allow_none=True)
id = fields.Integer(required=True)
portfolio_id = fields.Integer(required=True)
obligate_by = fields.Integer(allow_none=True)
projects = fields.List(fields.Nested(ProjectSchema()), default=[])


Expand Down Expand Up @@ -51,7 +64,7 @@ class FundingBudgetVersionSchema(Schema):
can_id = fields.Integer(required=True)
display_name = fields.String(allow_none=True)
fiscal_year = fields.Integer(required=True)
id = fields.Integer(required=True)
id = fields.Integer()
notes = fields.String(allow_none=True)
created_on = fields.DateTime(format="%Y-%m-%dT%H:%M:%S.%fZ", allow_none=True)
updated_on = fields.DateTime(format="%Y-%m-%dT%H:%M:%S.%fZ", allow_none=True)
Expand All @@ -70,7 +83,7 @@ class FundingBudgetSchema(Schema):
can_id = fields.Integer(required=True)
display_name = fields.String(allow_none=True)
fiscal_year = fields.Integer(required=True)
id = fields.Integer(required=True)
id = fields.Integer()
notes = fields.String(allow_none=True)
versions = fields.List(fields.Nested(FundingBudgetVersionSchema()), default=[])
created_on = fields.DateTime(format="%Y-%m-%dT%H:%M:%S.%fZ", allow_none=True)
Expand All @@ -90,7 +103,7 @@ class FundingDetailsSchema(Schema):
funding_partner = fields.String(allow_none=True)
funding_source = fields.String(allow_none=True)
id = fields.Integer(required=True)
method_of_transfer = fields.String(allow_none=True)
method_of_transfer = fields.Enum(CANMethodOfTransfer, allow_none=True)
sub_allowance = fields.String(allow_none=True)
created_on = fields.DateTime(format="%Y-%m-%dT%H:%M:%S.%fZ", allow_none=True)
updated_on = fields.DateTime(format="%Y-%m-%dT%H:%M:%S.%fZ", allow_none=True)
Expand Down
Loading

0 comments on commit 9fdcd34

Please sign in to comment.