Skip to content

Commit 366ef2c

Browse files
committed
Appendix backend
1 parent 6c24aeb commit 366ef2c

File tree

8 files changed

+353
-2
lines changed

8 files changed

+353
-2
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""appendix-fkey-inspection added
2+
3+
4+
Revision ID: 6e1dd95e1a4d
5+
Revises: e8a29d9b9a0d
6+
Create Date: 2025-02-26 11:43:42.768435
7+
8+
"""
9+
from alembic import op
10+
import sqlalchemy as sa
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = '6e1dd95e1a4d'
15+
down_revision = 'e8a29d9b9a0d'
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# ### commands auto generated by Alembic - please adjust! ###
22+
with op.batch_alter_table('appendices', schema=None) as batch_op:
23+
batch_op.add_column(sa.Column('inspection_id', sa.Integer(), nullable=False, comment='The unique identifier of the inspection'))
24+
batch_op.drop_constraint('appendices_appendix_no_key', type_='unique')
25+
batch_op.create_index('unique_non_deleted_appendix_number', ['inspection_id', 'appendix_no'], unique=True, postgresql_where=sa.text('is_deleted = false'))
26+
batch_op.create_foreign_key('appendix_inspection_id_inspection_id_fkey', 'inspections', ['inspection_id'], ['id'])
27+
28+
with op.batch_alter_table('appendices_version', schema=None) as batch_op:
29+
batch_op.add_column(sa.Column('inspection_id', sa.Integer(), autoincrement=False, nullable=True, comment='The unique identifier of the inspection'))
30+
batch_op.add_column(sa.Column('inspection_id_mod', sa.Boolean(), server_default=sa.text('false'), nullable=False))
31+
32+
# ### end Alembic commands ###
33+
34+
35+
def downgrade():
36+
# ### commands auto generated by Alembic - please adjust! ###
37+
with op.batch_alter_table('appendices_version', schema=None) as batch_op:
38+
batch_op.drop_column('inspection_id_mod')
39+
batch_op.drop_column('inspection_id')
40+
41+
with op.batch_alter_table('appendices', schema=None) as batch_op:
42+
batch_op.drop_constraint('appendix_inspection_id_inspection_id_fkey', type_='foreignkey')
43+
batch_op.drop_index('unique_non_deleted_appendix_number', postgresql_where=sa.text('is_deleted = false'))
44+
batch_op.create_unique_constraint('appendices_appendix_no_key', ['appendix_no'])
45+
batch_op.drop_column('inspection_id')
46+
47+
# ### end Alembic commands ###

compliance-api/src/compliance_api/models/appendix.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Model to handle appendices."""
22

3-
from sqlalchemy import Column, Integer, String
3+
from sqlalchemy import Boolean, Column, ForeignKey, Index, Integer, String
4+
from sqlalchemy.orm import relationship
45

56
from .base_model import BaseModelVersioned
67

@@ -11,5 +12,36 @@ class Appendix(BaseModelVersioned):
1112
__tablename__ = "appendices"
1213

1314
id = Column(Integer, primary_key=True, autoincrement=True)
14-
appendix_no = Column(Integer, unique=True, nullable=False)
15+
inspection_id = Column(
16+
Integer,
17+
ForeignKey("inspections.id", name="appendix_inspection_id_inspection_id_fkey"),
18+
nullable=False,
19+
comment="The unique identifier of the inspection",
20+
)
21+
appendix_no = Column(Integer, nullable=False)
1522
document_title = Column(String, nullable=False)
23+
is_deleted = Column(Boolean, default=False, server_default="f", nullable=False)
24+
inspection = relationship("Inspection", foreign_keys=[inspection_id], lazy="select")
25+
__table_args__ = (
26+
Index(
27+
"unique_non_deleted_appendix_number", # Index name
28+
"inspection_id",
29+
"appendix_no",
30+
unique=True,
31+
postgresql_where=(is_deleted is False), # Condition for uniqueness
32+
),
33+
)
34+
35+
@classmethod
36+
def get_by_no_nd_inspection(cls, appendix_no: int, inspection_id: int):
37+
"""Get appendix by name."""
38+
return cls.query.filter_by(
39+
appendix_no=appendix_no, inspection_id=inspection_id, is_deleted=False
40+
).first()
41+
42+
@classmethod
43+
def get_by_inspection_id(cls, inspection_id: int):
44+
"""Get all appendices by inspection id."""
45+
return cls.query.filter_by(
46+
inspection_id=inspection_id, is_deleted=False, is_active=True
47+
).all()

compliance-api/src/compliance_api/resources/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
from .agency import API as AGENCY_API
2727
from .apihelper import Api
28+
from .appendix import API as APPENDIX_API
2829
from .case_file import API as CASE_FILE_API
2930
from .complaint import API as COMPLAINT_API
3031
from .compliance_finding import API as COMPLIANCE_FINDING_API
@@ -93,3 +94,4 @@
9394
)
9495
API.add_namespace(DOCUMENT_TYPE_API)
9596
API.add_namespace(REQUIREMENT_TYPE_API)
97+
API.add_namespace(APPENDIX_API)
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Copyright © 2024 Province of British Columbia
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the 'License');
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an 'AS IS' BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""API endpoints for managing Appendix resource."""
15+
16+
from http import HTTPStatus
17+
18+
from flask import request
19+
from flask_restx import Namespace, Resource
20+
21+
from compliance_api.auth import auth
22+
from compliance_api.exceptions import ResourceNotFoundError
23+
from compliance_api.schemas import AppendixCreateSchema, AppendixSchema
24+
from compliance_api.services import AppendixService
25+
from compliance_api.utils.enum import PermissionEnum
26+
from compliance_api.utils.util import cors_preflight
27+
28+
from .apihelper import Api as ApiHelper
29+
30+
31+
API = Namespace("appendices", description="Endpoints for Appendix Management")
32+
33+
appendix_request_model = ApiHelper.convert_ma_schema_to_restx_model(
34+
API, AppendixCreateSchema(), "Appendix"
35+
)
36+
appendix_list_model = ApiHelper.convert_ma_schema_to_restx_model(
37+
API, AppendixSchema(), "AppendixList"
38+
)
39+
40+
41+
@cors_preflight("GET, OPTIONS, POST")
42+
@API.route("", methods=["POST", "GET", "OPTIONS"])
43+
class Appendices(Resource):
44+
"""Resource for managing appendices."""
45+
46+
@staticmethod
47+
@API.response(code=200, description="Success", model=[appendix_list_model])
48+
@API.doc(
49+
params={
50+
"inspection_id": {
51+
"description": "The unique identifier of the inspection",
52+
"type": "integer",
53+
"required": False,
54+
}
55+
}
56+
)
57+
@ApiHelper.swagger_decorators(API, endpoint_description="Fetch all appendices")
58+
@auth.require
59+
def get():
60+
"""Fetch all appendices."""
61+
inspection_id = request.args.get("inspection_id", None)
62+
if inspection_id:
63+
appendices = AppendixService.get_by_inspection_id(inspection_id)
64+
else:
65+
appendices = AppendixService.get_all()
66+
appendix_list_schema = AppendixSchema(many=True)
67+
return appendix_list_schema.dump(appendices), HTTPStatus.OK
68+
69+
@staticmethod
70+
@auth.require
71+
@ApiHelper.swagger_decorators(API, endpoint_description="Create an Appendix")
72+
@API.expect(appendix_request_model)
73+
@API.response(code=201, model=appendix_list_model, description="AppendixCreated")
74+
@API.response(400, "Bad Request")
75+
@auth.has_one_of_roles([PermissionEnum.SUPERUSER, PermissionEnum.ADMIN])
76+
def post():
77+
"""Create a Appendix."""
78+
appendix_data = AppendixCreateSchema().load(API.payload)
79+
created_appendix = AppendixService.create(appendix_data)
80+
return AppendixSchema().dump(created_appendix), HTTPStatus.CREATED
81+
82+
83+
@cors_preflight("GET, OPTIONS, PATCH, DELETE")
84+
@API.route("/<int:appendix_id>", methods=["PATCH", "GET", "OPTIONS", "DELETE"])
85+
@API.doc(params={"appendix_id": "The unique identifier of appendix"})
86+
class Appendix(Resource):
87+
"""Resource for managing a single Appendix."""
88+
89+
@staticmethod
90+
@auth.require
91+
@ApiHelper.swagger_decorators(API, endpoint_description="Fetch an appendix by id")
92+
@API.response(code=200, model=appendix_list_model, description="Success")
93+
@API.response(404, "Not Found")
94+
def get(appendix_id):
95+
"""Fetch an appendix by id."""
96+
appendix = AppendixService.get_by_id(appendix_id)
97+
if not appendix:
98+
raise ResourceNotFoundError(f"Appendix with {appendix_id} not found")
99+
return AppendixSchema().dump(Appendix), HTTPStatus.OK
100+
101+
@staticmethod
102+
@auth.require
103+
@ApiHelper.swagger_decorators(API, endpoint_description="Update an appendix by id")
104+
@API.expect(appendix_request_model)
105+
@API.response(code=200, model=appendix_list_model, description="Success")
106+
@API.response(400, "Bad Request")
107+
@API.response(404, "Not Found")
108+
@auth.has_one_of_roles([PermissionEnum.SUPERUSER, PermissionEnum.ADMIN])
109+
def patch(appendix_id):
110+
"""Update an Appendix by id."""
111+
appendix_data = AppendixCreateSchema().load(API.payload)
112+
updated_appendix = AppendixService.update(appendix_id, appendix_data)
113+
if not updated_appendix:
114+
raise ResourceNotFoundError(f"Appendix with {appendix_id} not found")
115+
return AppendixSchema().dump(updated_appendix), HTTPStatus.OK
116+
117+
@staticmethod
118+
@auth.require
119+
@ApiHelper.swagger_decorators(API, endpoint_description="Delete an appendix by id")
120+
@API.response(code=200, model=appendix_list_model, description="Deleted")
121+
@API.response(404, "Not Found")
122+
@auth.has_one_of_roles([PermissionEnum.SUPERUSER, PermissionEnum.ADMIN])
123+
def delete(appendix_id):
124+
"""Delete an appendix by id."""
125+
deleted_appendix = AppendixService.delete(appendix_id)
126+
if not deleted_appendix:
127+
raise ResourceNotFoundError(f"Appendix with {appendix_id} not found")
128+
return AppendixSchema().dump(deleted_appendix), HTTPStatus.OK

compliance-api/src/compliance_api/schemas/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414
"""Exposes all of the schemas in the compliance_api."""
1515
from .agency import AgencyCreateSchema, AgencySchema
16+
from .appendix import AppendixCreateSchema, AppendixSchema
1617
from .case_file import (
1718
CaseFileCreateSchema, CaseFileLinkCreateSchema, CaseFileLinkSchema, CaseFileOfficerSchema, CaseFileOptionSchema,
1819
CaseFileSchema, CaseFileStatusSchema, CaseFileUnlinkSchema, CaseFileUpdateSchema)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Copyright © 2024 Province of British Columbia
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the 'License');
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an 'AS IS' BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Appendix Schema."""
15+
from marshmallow import EXCLUDE, fields
16+
17+
from compliance_api.models.appendix import Appendix
18+
19+
from .base_schema import AutoSchemaBase, BaseSchema
20+
21+
22+
class AppendixSchema(AutoSchemaBase): # pylint: disable=too-many-ancestors
23+
"""Appendix schema."""
24+
25+
class Meta(AutoSchemaBase.Meta): # pylint: disable=too-few-public-methods
26+
"""Exclude unknown fields in the deserialized output."""
27+
28+
unknown = EXCLUDE
29+
model = Appendix
30+
include_fk = True
31+
32+
33+
class AppendixCreateSchema(BaseSchema): # pylint: disable=too-many-ancestors
34+
"""Appendix create Schema."""
35+
36+
appendix_no = fields.Integer(
37+
metadata={"description": "The unique number of appendix"},
38+
required=True,
39+
)
40+
inspection_id = fields.Integer(
41+
metadata={"description": "The inspection id"},
42+
required=True
43+
)
44+
document_title = fields.Str(
45+
metadata={"description": "The title of the document"},
46+
required=True
47+
)

compliance-api/src/compliance_api/services/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414
"""Exposes all of the Services used in the compliance_api."""
1515
from .agency import AgencyService
16+
from .appendix import AppendixService
1617
from .case_file import CaseFileService
1718
from .complaint import ComplaintService
1819
from .compliance_finding import ComplianceFindingService
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""Service for appendix management."""
2+
3+
from compliance_api.exceptions import ResourceExistsError, ResourceNotFoundError
4+
from compliance_api.models import db
5+
from compliance_api.models.appendix import Appendix as AppendixModel
6+
from compliance_api.models.inspection import Inspection as InspectionModel
7+
8+
9+
class AppendixService:
10+
"""Appendix management service."""
11+
12+
@classmethod
13+
def get_by_id(cls, appendix_id):
14+
"""Get appendix by id."""
15+
appendix = AppendixModel.find_by_id(appendix_id)
16+
return appendix
17+
18+
@classmethod
19+
def get_by_inspection_id(cls, inspection_id):
20+
"""Get appendix by id."""
21+
appendices = AppendixModel.get_by_inspection_id(inspection_id)
22+
return appendices
23+
24+
@classmethod
25+
def get_all(cls):
26+
"""Get all appendices."""
27+
appendices = AppendixModel.get_all(default_filters=False)
28+
return appendices
29+
30+
@classmethod
31+
def create(cls, appendix_data: dict, commit=True):
32+
"""Create appendix."""
33+
inspection_id = appendix_data.get("inspection_id")
34+
_check_existence_by_no(appendix_data.get("appendix_no"), inspection_id, None)
35+
_inspection_check(inspection_id)
36+
appendix = AppendixModel(**appendix_data)
37+
appendix.flush()
38+
if commit:
39+
db.session.commit()
40+
return appendix
41+
42+
@classmethod
43+
def update(cls, appendix_id, appendix_data, commit=True):
44+
"""Update appendix."""
45+
inspection_id = appendix_data.get("inspection_id")
46+
_check_existence_by_no(
47+
appendix_data.get("appendix_no"),
48+
inspection_id,
49+
appendix_id,
50+
)
51+
_inspection_check(appendix_data.get("inspection_id"))
52+
appendix = AppendixModel.find_by_id(appendix_id)
53+
if not appendix:
54+
return None
55+
appendix.update(appendix_data, commit=False)
56+
db.session.flush()
57+
if commit:
58+
db.session.commit()
59+
return appendix
60+
61+
@classmethod
62+
def delete(cls, agency_id, commit=True):
63+
"""Delete the appendix entity permenantly from database."""
64+
appendix = AppendixModel.find_by_id(agency_id)
65+
if not appendix:
66+
return None
67+
appendix.is_deleted = True
68+
appendix.is_active = False
69+
db.session.flush()
70+
if commit:
71+
db.session.commit()
72+
return appendix
73+
74+
75+
def _check_existence_by_no(
76+
appendix_no: str, inspection_id: int, appendix_id: int = None
77+
):
78+
"""Check if the appendix exists."""
79+
existing_appendix = AppendixModel.get_by_no_nd_inspection(
80+
appendix_no, inspection_id
81+
)
82+
if existing_appendix and (not appendix_id or existing_appendix.id != appendix_id):
83+
raise ResourceExistsError(f"Appendix with the number {appendix_no} exists")
84+
85+
86+
def _inspection_check(inspection_id):
87+
"""Check if the inspection and requirement exists."""
88+
inspection = InspectionModel.find_by_id(inspection_id)
89+
if not inspection:
90+
raise ResourceNotFoundError(
91+
f"Inspection with given ID {inspection_id} not found"
92+
)
93+
return inspection

0 commit comments

Comments
 (0)