Skip to content

Commit fc41454

Browse files
authored
specal field model and APIs (#1394)
- added new modal special_fields - added APIs to perform following operations: - Create - Update - Retrieve - Check for existing entry (validate)
1 parent e184854 commit fc41454

File tree

11 files changed

+542
-18
lines changed

11 files changed

+542
-18
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""special fields
2+
3+
Revision ID: 03791c319e2b
4+
Revises: 7a42d4f57279
5+
Create Date: 2023-12-01 12:39:54.325967
6+
7+
"""
8+
import sqlalchemy as sa
9+
from alembic import op
10+
from sqlalchemy.dialects import postgresql
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = '03791c319e2b'
15+
down_revision = '7a42d4f57279'
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# ### commands auto generated by Alembic - please adjust! ###
22+
op.create_table('special_fields',
23+
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
24+
sa.Column('entity', sa.Enum('PROJECT', 'PROPONENT', 'WORK', name='entityenum'), nullable=False),
25+
sa.Column('entity_id', sa.Integer(), nullable=False),
26+
sa.Column('field_name', sa.String(length=100), nullable=False),
27+
sa.Column('field_value', sa.String(), nullable=False),
28+
sa.Column('time_range', postgresql.TSTZRANGE(), nullable=False),
29+
sa.Column('created_by', sa.String(length=255), nullable=True),
30+
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text("TIMEZONE('utc', CURRENT_TIMESTAMP)"), nullable=True),
31+
sa.Column('updated_by', sa.String(length=255), nullable=True),
32+
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
33+
sa.Column('is_active', sa.Boolean(), server_default='t', nullable=False),
34+
sa.Column('is_deleted', sa.Boolean(), server_default='f', nullable=False),
35+
sa.PrimaryKeyConstraint('id')
36+
)
37+
with op.batch_alter_table('special_fields', schema=None) as batch_op:
38+
batch_op.create_index('entity_field_index', ['entity', 'entity_id', 'field_name', 'time_range'], unique=False)
39+
40+
op.create_table('special_fields_history',
41+
sa.Column('id', sa.Integer(), autoincrement=False, nullable=False),
42+
sa.Column('entity', sa.Enum('PROJECT', 'PROPONENT', 'WORK', name='entityenum'), autoincrement=False, nullable=False),
43+
sa.Column('entity_id', sa.Integer(), autoincrement=False, nullable=False),
44+
sa.Column('field_name', sa.String(length=100), autoincrement=False, nullable=False),
45+
sa.Column('field_value', sa.String(), autoincrement=False, nullable=False),
46+
sa.Column('time_range', postgresql.TSTZRANGE(), autoincrement=False, nullable=False),
47+
sa.Column('created_by', sa.String(length=255), autoincrement=False, nullable=True),
48+
sa.Column('created_at', sa.DateTime(timezone=True), autoincrement=False, nullable=True),
49+
sa.Column('updated_by', sa.String(length=255), autoincrement=False, nullable=True),
50+
sa.Column('updated_at', sa.DateTime(timezone=True), autoincrement=False, nullable=True),
51+
sa.Column('is_active', sa.Boolean(), autoincrement=False, nullable=False),
52+
sa.Column('is_deleted', sa.Boolean(), autoincrement=False, nullable=False),
53+
sa.Column('pk', sa.Integer(), autoincrement=True, nullable=False),
54+
sa.Column('during', postgresql.TSTZRANGE(), nullable=True),
55+
sa.PrimaryKeyConstraint('id', 'pk')
56+
)
57+
with op.batch_alter_table('special_fields_history', schema=None) as batch_op:
58+
batch_op.create_index('entity_field_history_index', ['entity', 'entity_id', 'field_name', 'time_range'], unique=False)
59+
60+
op.drop_table('project_special_fields')
61+
with op.batch_alter_table('event_templates', schema=None) as batch_op:
62+
batch_op.alter_column('visibility',
63+
existing_type=postgresql.ENUM('MANDATORY', 'OPTIONAL', 'HIDDEN', name='eventtemplatevisibilityenum'),
64+
comment='Indicate whether the event generated with this template should be autogenerated or available for optional events or added in the back end using actions',
65+
existing_comment='Indicate whether the event generated with this template should be autogenerated or available for optional events or added in the back end using actions',
66+
existing_nullable=False)
67+
68+
with op.batch_alter_table('event_templates_history', schema=None) as batch_op:
69+
batch_op.alter_column('visibility',
70+
existing_type=postgresql.ENUM('MANDATORY', 'OPTIONAL', 'HIDDEN', name='eventtemplatevisibilityenum'),
71+
comment='Indicate whether the event generated with this template should be autogenerated or available for optional events or added in the back end using actions',
72+
existing_comment='Indicate whether the event generated with this template should be autogenerated or available for optional events or added in the back end using actions',
73+
existing_nullable=False,
74+
autoincrement=False)
75+
76+
# ### end Alembic commands ###
77+
78+
79+
def downgrade():
80+
# ### commands auto generated by Alembic - please adjust! ###
81+
with op.batch_alter_table('event_templates_history', schema=None) as batch_op:
82+
batch_op.alter_column('visibility',
83+
existing_type=postgresql.ENUM('MANDATORY', 'OPTIONAL', 'HIDDEN', name='eventtemplatevisibilityenum'),
84+
comment='Indicate whether the event generated with this template should be autogenerated or available for optional events or added in the back end using actions',
85+
existing_comment='Indicate whether the event generated with this template should be autogenerated or available for optional events or added in the back end using actions',
86+
existing_nullable=False,
87+
autoincrement=False)
88+
89+
with op.batch_alter_table('event_templates', schema=None) as batch_op:
90+
batch_op.alter_column('visibility',
91+
existing_type=postgresql.ENUM('MANDATORY', 'OPTIONAL', 'HIDDEN', name='eventtemplatevisibilityenum'),
92+
comment='Indicate whether the event generated with this template should be autogenerated or available for optional events or added in the back end using actions',
93+
existing_comment='Indicate whether the event generated with this template should be autogenerated or available for optional events or added in the back end using actions',
94+
existing_nullable=False)
95+
96+
op.create_table('project_special_fields',
97+
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
98+
sa.Column('field_name', sa.VARCHAR(length=100), autoincrement=False, nullable=False),
99+
sa.Column('field_value', sa.VARCHAR(), autoincrement=False, nullable=False),
100+
sa.Column('time_range', postgresql.TSTZRANGE(), autoincrement=False, nullable=False),
101+
sa.Column('project_id', sa.INTEGER(), autoincrement=False, nullable=False),
102+
sa.Column('created_by', sa.VARCHAR(length=255), autoincrement=False, nullable=True),
103+
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text("timezone('utc'::text, CURRENT_TIMESTAMP)"), autoincrement=False, nullable=True),
104+
sa.Column('updated_by', sa.VARCHAR(length=255), autoincrement=False, nullable=True),
105+
sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True),
106+
sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=False),
107+
sa.Column('is_deleted', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=False),
108+
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], name='project_special_fields_project_id_fkey'),
109+
sa.PrimaryKeyConstraint('id', name='project_special_fields_pkey')
110+
)
111+
with op.batch_alter_table('special_fields_history', schema=None) as batch_op:
112+
batch_op.drop_index('entity_field_history_index')
113+
114+
op.drop_table('special_fields_history')
115+
with op.batch_alter_table('special_fields', schema=None) as batch_op:
116+
batch_op.drop_index('entity_field_index')
117+
118+
op.drop_table('special_fields')
119+
# ### end Alembic commands ###

epictrack-api/src/api/models/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,6 @@
4141
from .inspection_attachment import InspectionAttachment
4242
from .inspection_attendee import InspectionAttendee
4343
from .inspection_detail import InspectionDetail
44-
from .work_issues import WorkIssues
45-
from .work_issue_updates import WorkIssueUpdates
4644
from .milestone_type import MilestoneType
4745
from .ministry import Ministry
4846
from .outcome_configuration import OutcomeConfiguration
@@ -56,6 +54,7 @@
5654
from .reminder_configuration import ReminderConfiguration
5755
from .responsibility import Responsibility
5856
from .role import Role
57+
from .special_field import SpecialField
5958
from .staff import Staff
6059
from .staff_work_role import StaffWorkRole
6160
from .sub_types import SubType
@@ -68,6 +67,8 @@
6867
from .types import Type
6968
from .work import Work, WorkStateEnum
7069
from .work_calendar_event import WorkCalendarEvent
70+
from .work_issue_updates import WorkIssueUpdates
71+
from .work_issues import WorkIssues
7172
from .work_phase import WorkPhase
7273
from .work_status import WorkStatus
7374
from .work_type import WorkType

epictrack-api/src/api/models/project.py

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@
1515
import enum
1616

1717
from sqlalchemy import Boolean, Column, Enum, Float, ForeignKey, Integer, String, Text, func
18-
from sqlalchemy.dialects.postgresql import TSTZRANGE
1918
from sqlalchemy.orm import relationship
2019

21-
from .base_model import BaseModel, BaseModelVersioned
20+
from .base_model import BaseModelVersioned
2221

2322

2423
class ProjectStateEnum(enum.Enum):
@@ -88,17 +87,3 @@ def as_dict(self, recursive=True):
8887
data = super().as_dict(recursive)
8988
data["project_state"] = self.project_state.value
9089
return data
91-
92-
93-
class ProjectSpecialFields(BaseModel):
94-
"""Model class for tracking project special field values."""
95-
96-
__tablename__ = "project_special_fields"
97-
98-
id = Column(Integer, primary_key=True, autoincrement=True)
99-
field_name = Column(String(100), nullable=False)
100-
field_value = Column(String, nullable=False)
101-
time_range = Column(TSTZRANGE, nullable=False)
102-
103-
project_id = Column(ForeignKey("projects.id"), nullable=False)
104-
project = relationship("Project", foreign_keys=[project_id], lazy="select")
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Copyright © 2019 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+
"""Model to manage Special fields."""
15+
import enum
16+
17+
from sqlalchemy import Column, Enum, Index, Integer, String
18+
from sqlalchemy.dialects.postgresql import TSTZRANGE
19+
20+
from .base_model import BaseModelVersioned
21+
22+
23+
class EntityEnum(enum.Enum):
24+
"""Enum for enities"""
25+
26+
PROJECT = "PROJECT"
27+
PROPONENT = "PROPONENT"
28+
WORK = "WORK"
29+
30+
31+
class SpecialField(BaseModelVersioned):
32+
"""Model class for tracking special field values."""
33+
34+
__tablename__ = "special_fields"
35+
36+
id = Column(Integer, primary_key=True, autoincrement=True)
37+
entity = Column(Enum(EntityEnum), nullable=False)
38+
entity_id = Column(Integer, nullable=False)
39+
field_name = Column(String(100), nullable=False)
40+
field_value = Column(String, nullable=False)
41+
time_range = Column(TSTZRANGE, nullable=False)
42+
43+
__table_args__ = (Index('entity_field_index', "entity", "entity_id", "field_name", "time_range"), )

epictrack-api/src/api/resources/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from .reminder_configuration import API as REMINDER_CONFIGURATION_API
4343
from .reports import API as REPORTS_API
4444
from .responsibility import API as RESPONSIBILITY_API
45+
from .special_field import API as SPECIAL_FIELD_API
4546
from .staff import API as STAFF_API
4647
from .sub_types import API as SUB_TYPES_API
4748
from .sync_form_data import API as SYNC_FORM_DATA_API
@@ -52,6 +53,7 @@
5253
from .work_issues import API as WORK_ISSUES_API
5354
from .work_status import API as WORK_STATUS_API
5455

56+
5557
__all__ = ("API_BLUEPRINT", "OPS_BLUEPRINT")
5658

5759
# This will add the Authorize button to the swagger docs
@@ -107,3 +109,4 @@
107109
API.add_namespace(ACT_SECTION_API, path="/act-sections")
108110
API.add_namespace(WORK_STATUS_API, path='/work/<int:work_id>/statuses')
109111
API.add_namespace(WORK_ISSUES_API, path='/work/<int:work_id>/issues')
112+
API.add_namespace(SPECIAL_FIELD_API, path='/special-fields')
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Copyright © 2019 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+
"""Resource for Special field endpoints."""
15+
16+
from http import HTTPStatus
17+
18+
from flask import jsonify, request
19+
from flask_restx import Namespace, Resource, cors
20+
21+
from api.exceptions import ResourceNotFoundError
22+
from api.schemas import request as req
23+
from api.schemas import response as res
24+
from api.services.special_field import SpecialFieldService
25+
from api.utils import auth, constants
26+
from api.utils.caching import AppCache
27+
from api.utils.profiler import profiletime
28+
from api.utils.util import cors_preflight
29+
30+
31+
API = Namespace("special-fields", description="Special fields")
32+
33+
34+
@cors_preflight("GET, POST")
35+
@API.route("", methods=["GET", "POST", "OPTIONS"])
36+
class SpecialFields(Resource):
37+
"""Endpoint resource to return spcial fields values."""
38+
39+
@staticmethod
40+
@cors.crossdomain(origin="*")
41+
@auth.require
42+
@profiletime
43+
@AppCache.cache.cached(timeout=constants.CACHE_DAY_TIMEOUT, query_string=True)
44+
def get():
45+
"""Return all special field values based on params."""
46+
params = req.SpecialFieldQueryParamSchema().load(request.args)
47+
values = SpecialFieldService.find_all_by_params(params)
48+
return jsonify(res.SpecialFieldResponseSchema(many=True).dump(values)), HTTPStatus.OK
49+
50+
@staticmethod
51+
@cors.crossdomain(origin="*")
52+
@auth.require
53+
@profiletime
54+
def post():
55+
"""Create new task template"""
56+
request_json = req.SpecialFieldBodyParameterSchema().load(API.payload)
57+
entry = SpecialFieldService.create_special_field_entry(request_json)
58+
return res.SpecialFieldResponseSchema().dump(entry), HTTPStatus.CREATED
59+
60+
61+
@cors_preflight('GET, PUT')
62+
@API.route('/<int:special_field_id>', methods=['GET', 'PUT', 'OPTIONS'])
63+
class SpecialField(Resource):
64+
"""Endpoint resource to return special field details."""
65+
66+
@staticmethod
67+
@cors.crossdomain(origin='*')
68+
@auth.require
69+
@profiletime
70+
def get(special_field_id):
71+
"""Return a special field detail based on id."""
72+
req.SpecialFieldIdPathParameterSchema().load(request.view_args)
73+
special_field_entry = SpecialFieldService.find_by_id(special_field_id)
74+
if special_field_entry:
75+
return res.SpecialFieldResponseSchema().dump(special_field_entry), HTTPStatus.OK
76+
raise ResourceNotFoundError(f'Special field entry with id "{special_field_id}" not found')
77+
78+
@staticmethod
79+
@cors.crossdomain(origin='*')
80+
@auth.require
81+
@profiletime
82+
def put(special_field_id):
83+
"""Update and return a special field entry."""
84+
req.SpecialFieldIdPathParameterSchema().load(request.view_args)
85+
request_json = req.SpecialFieldBodyParameterSchema().load(API.payload)
86+
special_field_entry = SpecialFieldService.update_special_field_entry(special_field_id, request_json)
87+
return res.SpecialFieldResponseSchema().dump(special_field_entry), HTTPStatus.OK
88+
89+
90+
@cors_preflight('GET')
91+
@API.route('/exists', methods=['GET', 'OPTIONS'])
92+
class ValidateSpecialFieldEntry(Resource):
93+
"""Endpoint resource to check for existing special field entry."""
94+
95+
@staticmethod
96+
@cors.crossdomain(origin='*')
97+
@auth.require
98+
@profiletime
99+
def get():
100+
"""Checks for existing special field entries."""
101+
args = req.SpecialFieldExistanceQueryParamSchema().load(request.args)
102+
special_field_entry_id = args.pop('spcial_field_id')
103+
exists = SpecialFieldService.check_existence(args, special_field_id=special_field_entry_id)
104+
return (
105+
{'exists': exists},
106+
HTTPStatus.OK,
107+
)

epictrack-api/src/api/schemas/request/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
from .proponent_request import (
3737
ProponentBodyParameterSchema, ProponentExistenceQueryParamSchema, ProponentIdPathParameterSchema)
3838
from .reminder_configuration_request import ReminderConfigurationExistenceQueryParamSchema
39+
from .special_field_request import (
40+
SpecialFieldBodyParameterSchema, SpecialFieldExistanceQueryParamSchema, SpecialFieldIdPathParameterSchema,
41+
SpecialFieldQueryParamSchema)
3942
from .staff_request import (
4043
StaffBodyParameterSchema, StaffByPositionsQueryParamSchema, StaffEmailPathParameterSchema,
4144
StaffExistanceQueryParamSchema, StaffIdPathParameterSchema)

0 commit comments

Comments
 (0)