Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding /cans/ endpoints and service layer refactor #2831

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
fc198b7
Initial commit work for creation of /cans/<id> POST endpoint.
rajohnson90 Sep 11, 2024
da56e08
Adding can permissions to budget team and admin test user roles. Add…
rajohnson90 Sep 12, 2024
3d5c60d
Finished initial implementation of /cans/ POST endpoint, including un…
rajohnson90 Sep 13, 2024
43e35d2
Merge branch 'main' into OPS-2781/adding-cans-endpoints
rajohnson90 Sep 13, 2024
99eb7d4
Working on patch method for CANs. Added obligate_by date to the CAN s…
rajohnson90 Sep 17, 2024
61365e0
Merge branch 'main' into OPS-2781/adding-cans-endpoints
rajohnson90 Sep 18, 2024
93ab8ae
Initial complete version of PATCH /cans/ and unit tests
rajohnson90 Sep 19, 2024
502c214
Adding event handler to CAN create and update endpoints.
rajohnson90 Sep 19, 2024
a67afc6
Merge branch 'main' into OPS-2781/adding-cans-endpoints
rajohnson90 Sep 19, 2024
64c2f70
Adding obligate_by field to CAN schema.
rajohnson90 Sep 19, 2024
d62f70f
Updating CreateCANRequestSchema to be CreateUpdateCANRequestSchema an…
rajohnson90 Sep 19, 2024
ce39390
Changed obligate_by to an integer to fix serialization issues. Update…
rajohnson90 Sep 19, 2024
f8681ed
Merge branch 'OPS-2781/adding-cans-endpoints' into OPS-2781/adding-re…
rajohnson90 Sep 19, 2024
c4849a9
Added PUT /cans/ and tests. Renaming 'update_by_field' to just 'updat…
rajohnson90 Sep 19, 2024
97faf70
Adding DELETE /cans/ endpoint
rajohnson90 Sep 20, 2024
405e5c7
Refactoring GET endpoints to go through service layer and updating un…
rajohnson90 Sep 20, 2024
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,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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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