Skip to content

Commit

Permalink
Special Field Common Component bcgov#1359 (bcgov#1461)
Browse files Browse the repository at this point in the history
* Special Field Common Component bcgov#1359
  - Added new shared component for special field
  - bumped material-react-table version to 2.0.5
  - updated project service to add entries to special fields table when creating new project
  - updated special field service to update original table with latest value for the field on create/update operations

* updated to use sepcial field names from a const instead hardcoded strings
  • Loading branch information
salabh-aot committed Dec 19, 2023
1 parent 447a9b5 commit f957072
Show file tree
Hide file tree
Showing 21 changed files with 1,003 additions and 318 deletions.
13 changes: 13 additions & 0 deletions epictrack-api/src/api/models/special_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,16 @@ class SpecialField(BaseModelVersioned):
time_range = Column(TSTZRANGE, nullable=False)

__table_args__ = (Index('entity_field_index', "entity", "entity_id", "field_name", "time_range"), )

@classmethod
def find_by_params(cls, params: dict, default_filters=True):
"""Returns based on the params"""
query = {}
for key, value in params.items():
query[key] = value
if default_filters and hasattr(cls, 'is_active'):
query['is_active'] = True
if hasattr(cls, 'is_deleted'):
query['is_deleted'] = False
rows = cls.query.filter_by(**query).order_by(SpecialField.time_range.desc()).all()
return rows
24 changes: 1 addition & 23 deletions epictrack-api/src/api/resources/special_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@
from api.schemas import request as req
from api.schemas import response as res
from api.services.special_field import SpecialFieldService
from api.utils import auth, constants
from api.utils.caching import AppCache
from api.utils import auth
from api.utils.profiler import profiletime
from api.utils.util import cors_preflight

Expand All @@ -40,7 +39,6 @@ class SpecialFields(Resource):
@cors.crossdomain(origin="*")
@auth.require
@profiletime
@AppCache.cache.cached(timeout=constants.CACHE_DAY_TIMEOUT, query_string=True)
def get():
"""Return all special field values based on params."""
params = req.SpecialFieldQueryParamSchema().load(request.args)
Expand Down Expand Up @@ -85,23 +83,3 @@ def put(special_field_id):
request_json = req.SpecialFieldBodyParameterSchema().load(API.payload)
special_field_entry = SpecialFieldService.update_special_field_entry(special_field_id, request_json)
return res.SpecialFieldResponseSchema().dump(special_field_entry), HTTPStatus.OK


@cors_preflight('GET')
@API.route('/exists', methods=['GET', 'OPTIONS'])
class ValidateSpecialFieldEntry(Resource):
"""Endpoint resource to check for existing special field entry."""

@staticmethod
@cors.crossdomain(origin='*')
@auth.require
@profiletime
def get():
"""Checks for existing special field entries."""
args = req.SpecialFieldExistanceQueryParamSchema().load(request.args)
special_field_entry_id = args.pop('spcial_field_id')
exists = SpecialFieldService.check_existence(args, special_field_id=special_field_entry_id)
return (
{'exists': exists},
HTTPStatus.OK,
)
3 changes: 1 addition & 2 deletions epictrack-api/src/api/schemas/request/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@
ProponentBodyParameterSchema, ProponentExistenceQueryParamSchema, ProponentIdPathParameterSchema)
from .reminder_configuration_request import ReminderConfigurationExistenceQueryParamSchema
from .special_field_request import (
SpecialFieldBodyParameterSchema, SpecialFieldExistanceQueryParamSchema, SpecialFieldIdPathParameterSchema,
SpecialFieldQueryParamSchema)
SpecialFieldBodyParameterSchema, SpecialFieldIdPathParameterSchema, SpecialFieldQueryParamSchema)
from .staff_request import (
StaffBodyParameterSchema, StaffByPositionsQueryParamSchema, StaffEmailPathParameterSchema,
StaffExistanceQueryParamSchema, StaffIdPathParameterSchema)
Expand Down
58 changes: 6 additions & 52 deletions epictrack-api/src/api/schemas/request/special_field_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Special field resource's input validations"""
from marshmallow import fields, validate
from marshmallow import EXCLUDE, fields, validate

from api.models.special_field import EntityEnum

Expand Down Expand Up @@ -71,11 +71,11 @@ class SpecialFieldBodyParameterSchema(RequestBodyParameterSchema):
active_from = fields.DateTime(
metadata={"description": "Lower bound for time range"}, required=True
)
active_to = fields.DateTime(
metadata={"description": "Upper bound for time range"},
allow_none=True,
missing=None,
)

class Meta: # pylint: disable=too-few-public-methods
"""Meta information"""

unknown = EXCLUDE


class SpecialFieldIdPathParameterSchema(RequestPathParameterSchema):
Expand All @@ -86,49 +86,3 @@ class SpecialFieldIdPathParameterSchema(RequestPathParameterSchema):
validate=validate.Range(min=1),
required=True,
)


class SpecialFieldExistanceQueryParamSchema(RequestQueryParameterSchema):
"""Special field existance check query parameters"""

entity = fields.Str(
metadata={"description": "Entity name"},
required=True,
validate=validate.OneOf([x.value for x in EntityEnum]),
)

entity_id = fields.Int(
metadata={"description": "The id of the entity"},
validate=validate.Range(min=1),
required=True,
)

field_name = fields.Str(
metadata={"description": "Name of the special field"},
validate=validate.Length(max=150),
required=True,
)

field_value = fields.Str(
metadata={"description": "Value of the special field"},
validate=validate.Length(min=1),
required=True,
)

active_from = fields.DateTime(
metadata={"description": "Lower bound for time range"}, required=True
)

active_to = fields.DateTime(
metadata={"description": "Upper bound for time range"},
allow_none=True,
missing=None,
)

spcial_field_id = fields.Int(
metadata={"description": "The id of the special field entry"},
validate=validate.Range(min=1),
required=False,
allow_none=True,
missing=None
)
19 changes: 19 additions & 0 deletions epictrack-api/src/api/services/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@
from api.models.project import ProjectStateEnum
from api.models.proponent import Proponent
from api.models.region import Region
from api.models.special_field import EntityEnum
from api.models.sub_types import SubType
from api.models.types import Type
from api.models.work import Work
from api.models.work_type import WorkType
from api.schemas.types import TypeSchema
from api.services.special_field import SpecialFieldService
from api.utils.constants import PROJECT_STATE_ENUM_MAPS
from api.utils.enums import ProjectCodeMethod
from api.utils.token_info import TokenInfo
Expand Down Expand Up @@ -66,6 +68,23 @@ def create_project(cls, payload: dict):
project = Project(**payload)
project.project_state = ProjectStateEnum.PRE_WORK
current_app.logger.info(f"Project obj {dir(project)}")
project.flush()
proponent_special_field_data = {
"entity": EntityEnum.PROJECT,
"entity_id": project.id,
"field_name": "proponent_id",
"field_value": project.proponent_id,
"active_from": project.created_at
}
SpecialFieldService.create_special_field_entry(proponent_special_field_data)
project_name_special_field_data = {
"entity": EntityEnum.PROJECT,
"entity_id": project.id,
"field_name": "name",
"field_value": project.name,
"active_from": project.created_at
}
SpecialFieldService.create_special_field_entry(project_name_special_field_data)
project.save()
return project

Expand Down
83 changes: 59 additions & 24 deletions epictrack-api/src/api/services/special_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Service to manage Special fields."""
from datetime import datetime, timedelta
from typing import Union

from flask import current_app
from psycopg2.extras import DateTimeTZRange
from sqlalchemy import func, or_
from sqlalchemy.dialects.postgresql.ranges import Range

from api.exceptions import ResourceNotFoundError, UnprocessableEntityError
from api.exceptions import ResourceNotFoundError
from api.models import SpecialField, db
from api.utils.constants import SPECIAL_FIELD_ENTITY_MODEL_MAPS


class SpecialFieldService: # pylint:disable=too-few-public-methods
Expand All @@ -31,48 +37,77 @@ def find_all_by_params(cls, args: dict):
@classmethod
def create_special_field_entry(cls, payload: dict):
"""Create special field entry"""
# payload["time_range"] = DateTimeTZRange([payload["time_range"]] + ["[)"])
existing = cls.check_existence(payload)
if existing:
raise UnprocessableEntityError("Value with overlapping time range exists. Please fix it before continuing.")
upper_limit = cls._get_upper_limit(payload)
payload["time_range"] = DateTimeTZRange(
payload.pop("active_from"), payload.pop("active_to", None), "[)"
payload.pop("active_from"), upper_limit, bounds="[)"
)
entry = SpecialField(**payload)
entry.save()
return entry
special_field = SpecialField(**payload)
special_field.save()
cls._update_original_model(special_field)
db.session.commit()
return special_field

@classmethod
def update_special_field_entry(cls, special_field_id: int, payload: dict):
"""Create special field entry"""
exists = cls.check_existence(payload, special_field_id)
if exists:
raise UnprocessableEntityError("Value with overlapping time range exists. Please fix it before continuing.")
special_field = SpecialField.find_by_id(special_field_id)
upper_limit = cls._get_upper_limit(payload, special_field_id)

if not special_field:
raise ResourceNotFoundError(f"Special field entry with id '{special_field_id}' not found")
payload["time_range"] = DateTimeTZRange(
payload.pop("active_from"), payload.pop("active_to", None), "[)"
raise ResourceNotFoundError(
f"Special field entry with id '{special_field_id}' not found"
)
payload["time_range"] = Range(
payload.pop("active_from"), upper_limit, bounds="[)"
)
special_field = special_field.update(payload)
cls._update_original_model(special_field)
db.session.commit()
return special_field

@classmethod
def find_by_id(cls, _id):
"""Find special field entry by id."""
special_field = SpecialField.find_by_id(_id)
return special_field

@classmethod
def check_existence(cls, payload: dict, special_field_id: int = None) -> bool:
"""Validate time range"""
new_range = DateTimeTZRange(payload["active_from"], payload["active_to"], "[)")
def _get_upper_limit(
cls, payload: dict, special_field_id: int = None
) -> Union[datetime, None]:
"""Finds and returns the upper limit of time range and updates existing entries to match new time range"""
exists_query = db.session.query(SpecialField).filter(
SpecialField.entity == payload["entity"],
SpecialField.entity_id == payload["entity_id"],
SpecialField.field_name == payload["field_name"],
SpecialField.time_range.overlaps(new_range),
or_(
SpecialField.time_range.contains(payload["active_from"]),
func.lower(SpecialField.time_range) > payload["active_from"],
),
)
if special_field_id:
exists_query = exists_query.filter(SpecialField.id != special_field_id)
return bool(exists_query.first())
existing_entry = exists_query.order_by(SpecialField.time_range.asc()).first()
upper_limit = None
if existing_entry:
if existing_entry.time_range.lower > payload["active_from"]:
upper_limit = existing_entry.time_range.lower + timedelta(days=-1)
else:
upper_limit = existing_entry.time_range.upper
new_range = DateTimeTZRange(
existing_entry.time_range.lower,
payload["active_from"] + timedelta(days=-1),
"[)",
)
existing_entry.time_range = new_range
db.session.add(existing_entry)
return upper_limit

@classmethod
def find_by_id(cls, _id):
"""Find special field entry by id."""
special_field = SpecialField.find_by_id(_id)
return special_field
def _update_original_model(cls, special_field_entry: SpecialField) -> None:
"""If `special_field_entry` is latest, update original table with new value"""
if special_field_entry.time_range.upper is None:
model_class = SPECIAL_FIELD_ENTITY_MODEL_MAPS[special_field_entry.entity]
model_class.query.filter(
model_class.id == special_field_entry.entity_id
).update({special_field_entry.field_name: special_field_entry.field_value})
11 changes: 10 additions & 1 deletion epictrack-api/src/api/utils/constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""File representing constants used in the application"""

from api.models.project import ProjectStateEnum
from api.models.project import Project, ProjectStateEnum
from api.models.proponent import Proponent
from api.models.special_field import EntityEnum
from api.models.work import Work


SCHEMA_MAPS = {
Expand All @@ -27,3 +30,9 @@
}

PIP_LINK_URL_BASE = "https://apps.nrs.gov.bc.ca/int/fnp/FirstNationDetail.xhtml?name="

SPECIAL_FIELD_ENTITY_MODEL_MAPS = {
EntityEnum.PROJECT: Project,
EntityEnum.WORK: Work,
EntityEnum.PROPONENT: Proponent,
}
Loading

0 comments on commit f957072

Please sign in to comment.