Skip to content

Commit 0c73bfc

Browse files
authored
Merge pull request #232 from dinesh-aot/COMP-356
Appendix backend
2 parents b96ed67 + 366ef2c commit 0c73bfc

File tree

11 files changed

+442
-1
lines changed

11 files changed

+442
-1
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 ###
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Appendix table creation
2+
3+
Revision ID: e8a29d9b9a0d
4+
Revises: 231cb1f8b9e7
5+
Create Date: 2025-02-22 23:34:10.952601
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = 'e8a29d9b9a0d'
14+
down_revision = '231cb1f8b9e7'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.create_table('appendices',
22+
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
23+
sa.Column('appendix_no', sa.Integer(), nullable=False),
24+
sa.Column('document_title', sa.String(), nullable=False),
25+
sa.Column('created_date', sa.DateTime(), nullable=False),
26+
sa.Column('updated_date', sa.DateTime(), nullable=True),
27+
sa.Column('created_by', sa.String(length=100), nullable=False),
28+
sa.Column('updated_by', sa.String(length=100), nullable=True),
29+
sa.Column('is_active', sa.Boolean(), server_default='t', nullable=False),
30+
sa.Column('is_deleted', sa.Boolean(), server_default='f', nullable=False),
31+
sa.PrimaryKeyConstraint('id'),
32+
sa.UniqueConstraint('appendix_no')
33+
)
34+
op.create_table('appendices_version',
35+
sa.Column('id', sa.Integer(), autoincrement=False, nullable=False),
36+
sa.Column('appendix_no', sa.Integer(), autoincrement=False, nullable=True),
37+
sa.Column('document_title', sa.String(), autoincrement=False, nullable=True),
38+
sa.Column('created_date', sa.DateTime(), autoincrement=False, nullable=True),
39+
sa.Column('updated_date', sa.DateTime(), autoincrement=False, nullable=True),
40+
sa.Column('created_by', sa.String(length=100), autoincrement=False, nullable=True),
41+
sa.Column('updated_by', sa.String(length=100), autoincrement=False, nullable=True),
42+
sa.Column('is_active', sa.Boolean(), server_default='t', autoincrement=False, nullable=True),
43+
sa.Column('is_deleted', sa.Boolean(), server_default='f', autoincrement=False, nullable=True),
44+
sa.Column('transaction_id', sa.BigInteger(), autoincrement=False, nullable=False),
45+
sa.Column('end_transaction_id', sa.BigInteger(), nullable=True),
46+
sa.Column('operation_type', sa.SmallInteger(), nullable=False),
47+
sa.Column('appendix_no_mod', sa.Boolean(), server_default=sa.text('false'), nullable=False),
48+
sa.Column('document_title_mod', sa.Boolean(), server_default=sa.text('false'), nullable=False),
49+
sa.Column('created_date_mod', sa.Boolean(), server_default=sa.text('false'), nullable=False),
50+
sa.Column('updated_date_mod', sa.Boolean(), server_default=sa.text('false'), nullable=False),
51+
sa.Column('created_by_mod', sa.Boolean(), server_default=sa.text('false'), nullable=False),
52+
sa.Column('updated_by_mod', sa.Boolean(), server_default=sa.text('false'), nullable=False),
53+
sa.Column('is_active_mod', sa.Boolean(), server_default=sa.text('false'), nullable=False),
54+
sa.Column('is_deleted_mod', sa.Boolean(), server_default=sa.text('false'), nullable=False),
55+
sa.PrimaryKeyConstraint('id', 'transaction_id')
56+
)
57+
with op.batch_alter_table('appendices_version', schema=None) as batch_op:
58+
batch_op.create_index(batch_op.f('ix_appendices_version_end_transaction_id'), ['end_transaction_id'], unique=False)
59+
batch_op.create_index(batch_op.f('ix_appendices_version_operation_type'), ['operation_type'], unique=False)
60+
batch_op.create_index(batch_op.f('ix_appendices_version_transaction_id'), ['transaction_id'], unique=False)
61+
62+
# ### end Alembic commands ###
63+
64+
65+
def downgrade():
66+
# ### commands auto generated by Alembic - please adjust! ###
67+
with op.batch_alter_table('appendices_version', schema=None) as batch_op:
68+
batch_op.drop_index(batch_op.f('ix_appendices_version_transaction_id'))
69+
batch_op.drop_index(batch_op.f('ix_appendices_version_operation_type'))
70+
batch_op.drop_index(batch_op.f('ix_appendices_version_end_transaction_id'))
71+
72+
op.drop_table('appendices_version')
73+
op.drop_table('appendices')
74+
# ### end Alembic commands ###

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""This exports all of the models and schemas used by the application."""
1616

1717
from .agency import Agency
18+
from .appendix import Appendix
1819
from .case_file import (
1920
CaseFile, CaseFileInitiationEnum, CaseFileInitiationOption, CaseFileLink, CaseFileOfficer, CaseFileStatusEnum)
2021
from .complaint import (
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Model to handle appendices."""
2+
3+
from sqlalchemy import Boolean, Column, ForeignKey, Index, Integer, String
4+
from sqlalchemy.orm import relationship
5+
6+
from .base_model import BaseModelVersioned
7+
8+
9+
class Appendix(BaseModelVersioned):
10+
"""Definition of the Appendix entity."""
11+
12+
__tablename__ = "appendices"
13+
14+
id = Column(Integer, primary_key=True, autoincrement=True)
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)
22+
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/models/case_file.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ def get_max_case_file_number_by_year(cls, year: int):
180180
f"^{year}[0-9]{{4}}$"
181181
),
182182
cls.is_active.is_(True),
183-
cls.is_deleted.is_(False)
183+
cls.is_deleted.is_(False),
184184
)
185185
.scalar()
186186
)

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

0 commit comments

Comments
 (0)