diff --git a/epictrack-api/migrations/versions/88b0bbb53b72_unique_abbreviation.py b/epictrack-api/migrations/versions/88b0bbb53b72_unique_abbreviation.py new file mode 100644 index 000000000..023d4880b --- /dev/null +++ b/epictrack-api/migrations/versions/88b0bbb53b72_unique_abbreviation.py @@ -0,0 +1,30 @@ +"""Add unique constraint to projects abbreviation + +Revision ID: 88b0bbb53b72 +Revises: 03791c319e2b +Create Date: 2023-12-05 11:34:09.723702 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '88b0bbb53b72' +down_revision = '03791c319e2b' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('projects', schema=None) as batch_op: + batch_op.create_unique_constraint('uq_projects_abbreviation', ['abbreviation']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('projects', schema=None) as batch_op: + batch_op.drop_constraint('uq_projects_abbreviation', type_='unique') + # ### end Alembic commands ### diff --git a/epictrack-api/src/api/models/project.py b/epictrack-api/src/api/models/project.py index 3f0a2a443..a37c2c57b 100644 --- a/epictrack-api/src/api/models/project.py +++ b/epictrack-api/src/api/models/project.py @@ -63,7 +63,7 @@ class Project(BaseModelVersioned): proponent_id = Column(ForeignKey("proponents.id"), nullable=False) region_id_env = Column(ForeignKey("regions.id"), nullable=True) region_id_flnro = Column(ForeignKey("regions.id"), nullable=True) - abbreviation = Column(String(10), nullable=True) + abbreviation = Column(String(10), nullable=True, unique=True) sub_type = relationship("SubType", foreign_keys=[sub_type_id], lazy="select") type = relationship("Type", foreign_keys=[type_id], lazy="select") proponent = relationship("Proponent", foreign_keys=[proponent_id], lazy="select") @@ -82,6 +82,11 @@ def check_existence(cls, name, project_id=None): return True return False + @classmethod + def get_by_abbreviation(cls, abbreviation: str): + """Get project by abbreviation.""" + return Project.query.filter_by(abbreviation=abbreviation).first() + def as_dict(self, recursive=True): """Return JSON Representation.""" data = super().as_dict(recursive) diff --git a/epictrack-api/src/api/resources/project.py b/epictrack-api/src/api/resources/project.py index 8628eb51a..37049b830 100644 --- a/epictrack-api/src/api/resources/project.py +++ b/epictrack-api/src/api/resources/project.py @@ -1,189 +1,205 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Resource for project endpoints.""" -from http import HTTPStatus - -from flask import jsonify, request -from flask_restx import Namespace, Resource, cors - -from api.schemas import request as req -from api.schemas import response as res -from api.schemas.work_type import WorkTypeSchema -from api.services import ProjectService -from api.utils import auth, profiletime -from api.utils.util import cors_preflight - - -API = Namespace("projects", description="Projects") - - -@cors_preflight("GET, DELETE, POST") -@API.route("", methods=["GET", "POST", "OPTIONS"]) -class Projects(Resource): - """Endpoint resource to manage projects.""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def get(): - """Return all projects.""" - projects = ProjectService.find_all() - return ( - jsonify(res.ProjectResponseSchema(many=True).dump(projects)), - HTTPStatus.OK, - ) - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def post(): - """Create new project""" - request_json = req.ProjectBodyParameterSchema().load(API.payload) - project = ProjectService.create_project(request_json) - return res.ProjectResponseSchema().dump(project), HTTPStatus.CREATED - - -@cors_preflight("GET, DELETE, PUT") -@API.route("/", methods=["GET", "PUT", "DELETE", "OPTIONS"]) -class Project(Resource): - """Endpoint resource to manage a project.""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def get(project_id): - """Return details of a project.""" - req.ProjectIdPathParameterSchema().load(request.view_args) - project = ProjectService.find(project_id) - return res.ProjectResponseSchema().dump(project), HTTPStatus.OK - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def put(project_id): - """Update and return a project.""" - req.ProjectIdPathParameterSchema().load(request.view_args) - request_json = req.ProjectBodyParameterSchema().load(API.payload) - project = ProjectService.update_project(project_id, request_json) - return res.ProjectResponseSchema().dump(project), HTTPStatus.OK - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def delete(project_id): - """Delete a project""" - req.ProjectIdPathParameterSchema().load(request.view_args) - ProjectService.delete_project(project_id) - return "Project successfully deleted", HTTPStatus.OK - - -@cors_preflight("GET") -@API.route("/exists", methods=["GET", "OPTIONS"]) -class ValidateProject(Resource): - """Endpoint resource to check for existing project.""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def get(): - """Checks for existing projects.""" - args = req.ProjectExistenceQueryParamSchema().load(request.args) - name = args["name"] - project_id = args["project_id"] - exists = ProjectService.check_existence(name=name, project_id=project_id) - return {"exists": exists}, HTTPStatus.OK - - -@cors_preflight("GET") -@API.route("//work-types", methods=["GET", "OPTIONS"]) -class ProjectWorkTypes(Resource): - """Endpoint resource to get all work types associated with a project.""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def get(project_id): - """Return work types associated with a project.""" - req.ProjectIdPathParameterSchema().load(request.view_args) - args = req.WorkIdPathParameterSchema().load(request.args) - work_id = args["work_id"] - work_types = ProjectService.find_project_work_types(project_id, work_id) - return WorkTypeSchema(many=True).dump(work_types), HTTPStatus.OK - - -@cors_preflight("GET") -@API.route("//first-nations", methods=["GET", "OPTIONS"]) -class ProjectFirstNations(Resource): - """Endpoint resource to get all first nations associated with a project.""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def get(project_id): - """Return first nations associated with a project.""" - req.ProjectIdPathParameterSchema().load(request.view_args) - args = req.ProjectFirstNationsQueryParamSchema().load(request.args) - work_type_id = args["work_type_id"] - work_id = args["work_id"] - first_nations = ProjectService.find_first_nations( - project_id, work_id, work_type_id - ) - return ( - res.IndigenousResponseNationSchema(many=True).dump(first_nations), - HTTPStatus.OK, - ) - - -@cors_preflight("GET") -@API.route("//first-nation-available", methods=["GET", "OPTIONS"]) -class ProjectFirstNationAvailableStatus(Resource): - """Endpoint resource to check if there are first nations associated with a project.""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def get(project_id): - """Check if any first nations associated with given project.""" - req.ProjectIdPathParameterSchema().load(request.view_args) - args = req.ProjectFirstNationsQueryParamSchema().load(request.args) - work_id = args["work_id"] - first_nation_availability = ProjectService.check_first_nation_available( - project_id, work_id - ) - return first_nation_availability, HTTPStatus.OK - - -@cors_preflight("POST") -@API.route("/import", methods=["POST", "OPTIONS"]) -class ImportProjects(Resource): - """Endpoint resource to import projects.""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def post(): - """Import projects""" - file = request.files["file"] - response = ProjectService.import_projects(file) - return response, HTTPStatus.CREATED +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Resource for project endpoints.""" +from http import HTTPStatus + +from flask import jsonify, request +from flask_restx import Namespace, Resource, cors + +from api.schemas import request as req +from api.schemas import response as res +from api.schemas.work_type import WorkTypeSchema +from api.services import ProjectService +from api.utils import auth, profiletime +from api.utils.util import cors_preflight + + +API = Namespace("projects", description="Projects") + + +@cors_preflight("GET, DELETE, POST") +@API.route("", methods=["GET", "POST", "OPTIONS"]) +class Projects(Resource): + """Endpoint resource to manage projects.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def get(): + """Return all projects.""" + projects = ProjectService.find_all() + return ( + jsonify(res.ProjectResponseSchema(many=True).dump(projects)), + HTTPStatus.OK, + ) + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def post(): + """Create new project""" + request_json = req.ProjectBodyParameterSchema().load(API.payload) + project = ProjectService.create_project(request_json) + return res.ProjectResponseSchema().dump(project), HTTPStatus.CREATED + + +@cors_preflight("GET, DELETE, PUT") +@API.route("/", methods=["GET", "PUT", "DELETE", "OPTIONS"]) +class Project(Resource): + """Endpoint resource to manage a project.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def get(project_id): + """Return details of a project.""" + req.ProjectIdPathParameterSchema().load(request.view_args) + project = ProjectService.find(project_id) + return res.ProjectResponseSchema().dump(project), HTTPStatus.OK + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def put(project_id): + """Update and return a project.""" + req.ProjectIdPathParameterSchema().load(request.view_args) + request_json = req.ProjectBodyParameterSchema().load(API.payload) + project = ProjectService.update_project(project_id, request_json) + return res.ProjectResponseSchema().dump(project), HTTPStatus.OK + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def delete(project_id): + """Delete a project""" + req.ProjectIdPathParameterSchema().load(request.view_args) + ProjectService.delete_project(project_id) + return "Project successfully deleted", HTTPStatus.OK + + +@cors_preflight("GET") +@API.route("/exists", methods=["GET", "OPTIONS"]) +class ValidateProject(Resource): + """Endpoint resource to check for existing project.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def get(): + """Checks for existing projects.""" + args = req.ProjectExistenceQueryParamSchema().load(request.args) + name = args["name"] + project_id = args["project_id"] + exists = ProjectService.check_existence(name=name, project_id=project_id) + return {"exists": exists}, HTTPStatus.OK + + +@cors_preflight("GET") +@API.route("//work-types", methods=["GET", "OPTIONS"]) +class ProjectWorkTypes(Resource): + """Endpoint resource to get all work types associated with a project.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def get(project_id): + """Return work types associated with a project.""" + req.ProjectIdPathParameterSchema().load(request.view_args) + args = req.WorkIdPathParameterSchema().load(request.args) + work_id = args["work_id"] + work_types = ProjectService.find_project_work_types(project_id, work_id) + return WorkTypeSchema(many=True).dump(work_types), HTTPStatus.OK + + +@cors_preflight("GET") +@API.route("//first-nations", methods=["GET", "OPTIONS"]) +class ProjectFirstNations(Resource): + """Endpoint resource to get all first nations associated with a project.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def get(project_id): + """Return first nations associated with a project.""" + req.ProjectIdPathParameterSchema().load(request.view_args) + args = req.ProjectFirstNationsQueryParamSchema().load(request.args) + work_type_id = args["work_type_id"] + work_id = args["work_id"] + first_nations = ProjectService.find_first_nations( + project_id, work_id, work_type_id + ) + return ( + res.IndigenousResponseNationSchema(many=True).dump(first_nations), + HTTPStatus.OK, + ) + + +@cors_preflight("GET") +@API.route("//first-nation-available", methods=["GET", "OPTIONS"]) +class ProjectFirstNationAvailableStatus(Resource): + """Endpoint resource to check if there are first nations associated with a project.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def get(project_id): + """Check if any first nations associated with given project.""" + req.ProjectIdPathParameterSchema().load(request.view_args) + args = req.ProjectFirstNationsQueryParamSchema().load(request.args) + work_id = args["work_id"] + first_nation_availability = ProjectService.check_first_nation_available( + project_id, work_id + ) + return first_nation_availability, HTTPStatus.OK + + +@cors_preflight("POST") +@API.route("/import", methods=["POST", "OPTIONS"]) +class ImportProjects(Resource): + """Endpoint resource to import projects.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def post(): + """Import projects""" + file = request.files["file"] + response = ProjectService.import_projects(file) + return response, HTTPStatus.CREATED + + +@cors_preflight("POST") +@API.route("/abbreviation", methods=["POST", "OPTIONS"]) +class ProjectAbbreviation(Resource): + """Endpoint resource to import projects.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def post(): + """Create new project abbreviation""" + request_json = req.ProjectAbbreviationParameterSchema().load(API.payload) + project_abbreviation = ProjectService.create_project_abbreviation(request_json.get("name")) + return project_abbreviation, HTTPStatus.CREATED diff --git a/epictrack-api/src/api/schemas/request/__init__.py b/epictrack-api/src/api/schemas/request/__init__.py index f04ee6f99..a1d43af64 100644 --- a/epictrack-api/src/api/schemas/request/__init__.py +++ b/epictrack-api/src/api/schemas/request/__init__.py @@ -31,8 +31,8 @@ from .outcome_template_request import OutcomeTemplateBodyParameterSchema from .phase_request import PhaseBodyParameterSchema from .project_request import ( - ProjectBodyParameterSchema, ProjectExistenceQueryParamSchema, ProjectFirstNationsQueryParamSchema, - ProjectIdPathParameterSchema) + ProjectAbbreviationParameterSchema, ProjectBodyParameterSchema, ProjectExistenceQueryParamSchema, + ProjectFirstNationsQueryParamSchema, ProjectIdPathParameterSchema) from .proponent_request import ( ProponentBodyParameterSchema, ProponentExistenceQueryParamSchema, ProponentIdPathParameterSchema) from .reminder_configuration_request import ReminderConfigurationExistenceQueryParamSchema diff --git a/epictrack-api/src/api/schemas/request/project_request.py b/epictrack-api/src/api/schemas/request/project_request.py index 3d0a7f01d..e0c70a248 100644 --- a/epictrack-api/src/api/schemas/request/project_request.py +++ b/epictrack-api/src/api/schemas/request/project_request.py @@ -1,164 +1,175 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Project resource's input validations""" -from marshmallow import fields, validate - -from .base import RequestBodyParameterSchema, RequestPathParameterSchema, RequestQueryParameterSchema - - -class ProjectBodyParameterSchema(RequestBodyParameterSchema): - """Project request body schema""" - - name = fields.Str( - metadata={"description": "Name of project"}, - validate=validate.Length(max=150), - required=True, - ) - latitude = fields.Float( - metadata={"description": "Latitude of project location"}, - validate=validate.Range(min=-90, max=90), - required=True, - ) - longitude = fields.Float( - metadata={"description": "Longitude of project location"}, - validate=validate.Range(min=-180, max=180), - required=True, - ) - - capital_investment = fields.Int( - metadata={"description": "Capital investment of project"}, - validate=validate.Range(min=0), - allow_none=True, - load_default=None, - ) - epic_guid = fields.Str( - metadata={"description": "EPIC GUID of project"}, - validate=validate.Length(max=150), - allow_none=True, - load_default=None, - ) - ea_certificate = fields.Str( - metadata={"description": "EA Certificate # of project"}, - validate=validate.Length(max=150), - allow_none=True, - load_default=None, - ) - abbreviation = fields.Str( - metadata={"description": "Abbreviation of the project"}, - validate=validate.Length(max=150), - allow_none=True, - load_default=None, - ) - description = fields.Str( - metadata={"description": "Description of project"}, - validate=validate.Length(max=2000), - required=True, - ) - address = fields.Str( - metadata={"description": "Location description of project"}, - validate=validate.Length(max=2000), - allow_none=True, - load_default=None, - ) - - proponent_id = fields.Int( - metadata={"description": "Proponent id of the project"}, - validate=validate.Range(min=1), - required=True - ) - - type_id = fields.Int( - metadata={"description": "Type id of the project"}, - validate=validate.Range(min=1), - required=True - ) - - sub_type_id = fields.Int( - metadata={"description": "SubType id of the project"}, - validate=validate.Range(min=1), - required=True - ) - - region_id_env = fields.Int( - metadata={"description": "ENV Region id of the project"}, - validate=validate.Range(min=1), - allow_none=True, - load_default=None - ) - region_id_flnro = fields.Int( - metadata={"description": "NRS Region id of the project"}, - validate=validate.Range(min=1), - allow_none=True, - load_default=None - ) - - fte_positions_construction = fields.Int( - metadata={"description": "FTE Positions created during construction on project"}, - validate=validate.Range(min=0), - allow_none=True, - load_default=None - ) - - fte_positions_operation = fields.Int( - metadata={"description": "FTE Positions created during operation on project"}, - validate=validate.Range(min=0), - allow_none=True, - load_default=None - ) - - is_active = fields.Bool(metadata={"description": "Active state of the project"}) - is_project_closed = fields.Bool(metadata={"description": "Closed state of the project"}, default=False) - - -class ProjectExistenceQueryParamSchema(RequestQueryParameterSchema): - """Project existence check query parameters""" - - name = fields.Str( - metadata={"description": "Name of the project"}, - validate=validate.Length(max=150), - required=True - ) - - project_id = fields.Int( - metadata={"description": "The id of the project"}, - validate=validate.Range(min=1), - load_default=None - ) - - -class ProjectIdPathParameterSchema(RequestPathParameterSchema): - """project id path parameter schema""" - - project_id = fields.Int( - metadata={"description": "The id of the project"}, - validate=validate.Range(min=1), - required=True - ) - - -class ProjectFirstNationsQueryParamSchema(RequestQueryParameterSchema): - """Project first nations query parameters""" - - work_id = fields.Int( - metadata={"description": "The id of the work"}, - validate=validate.Range(min=1), - required=True - ) - work_type_id = fields.Int( - metadata={"description": "The id of the work type"}, - validate=validate.Range(min=1), - load_default=None, - allow_none=True, - missing=None - ) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Project resource's input validations""" +from marshmallow import fields, validate + +from api.schemas.validators import is_uppercase + +from .base import RequestBodyParameterSchema, RequestPathParameterSchema, RequestQueryParameterSchema + + +class ProjectBodyParameterSchema(RequestBodyParameterSchema): + """Project request body schema""" + + name = fields.Str( + metadata={"description": "Name of project"}, + validate=validate.Length(max=150), + required=True, + ) + latitude = fields.Float( + metadata={"description": "Latitude of project location"}, + validate=validate.Range(min=-90, max=90), + required=True, + ) + longitude = fields.Float( + metadata={"description": "Longitude of project location"}, + validate=validate.Range(min=-180, max=180), + required=True, + ) + + capital_investment = fields.Int( + metadata={"description": "Capital investment of project"}, + validate=validate.Range(min=0), + allow_none=True, + load_default=None, + ) + epic_guid = fields.Str( + metadata={"description": "EPIC GUID of project"}, + validate=validate.Length(max=150), + allow_none=True, + load_default=None, + ) + ea_certificate = fields.Str( + metadata={"description": "EA Certificate # of project"}, + validate=validate.Length(max=150), + allow_none=True, + load_default=None, + ) + abbreviation = fields.Str( + metadata={"description": "Abbreviation of the project"}, + validate=[validate.Length(max=150), is_uppercase], + required=True, + ) + description = fields.Str( + metadata={"description": "Description of project"}, + validate=validate.Length(max=2000), + required=True, + ) + address = fields.Str( + metadata={"description": "Location description of project"}, + validate=validate.Length(max=2000), + allow_none=True, + load_default=None, + ) + + proponent_id = fields.Int( + metadata={"description": "Proponent id of the project"}, + validate=validate.Range(min=1), + required=True + ) + + type_id = fields.Int( + metadata={"description": "Type id of the project"}, + validate=validate.Range(min=1), + required=True + ) + + sub_type_id = fields.Int( + metadata={"description": "SubType id of the project"}, + validate=validate.Range(min=1), + required=True + ) + + region_id_env = fields.Int( + metadata={"description": "ENV Region id of the project"}, + validate=validate.Range(min=1), + allow_none=True, + load_default=None + ) + region_id_flnro = fields.Int( + metadata={"description": "NRS Region id of the project"}, + validate=validate.Range(min=1), + allow_none=True, + load_default=None + ) + + fte_positions_construction = fields.Int( + metadata={"description": "FTE Positions created during construction on project"}, + validate=validate.Range(min=0), + allow_none=True, + load_default=None + ) + + fte_positions_operation = fields.Int( + metadata={"description": "FTE Positions created during operation on project"}, + validate=validate.Range(min=0), + allow_none=True, + load_default=None + ) + + is_active = fields.Bool(metadata={"description": "Active state of the project"}) + is_project_closed = fields.Bool(metadata={"description": "Closed state of the project"}, default=False) + + +class ProjectExistenceQueryParamSchema(RequestQueryParameterSchema): + """Project existence check query parameters""" + + name = fields.Str( + metadata={"description": "Name of the project"}, + validate=validate.Length(max=150), + required=True + ) + + project_id = fields.Int( + metadata={"description": "The id of the project"}, + validate=validate.Range(min=1), + load_default=None + ) + + +class ProjectIdPathParameterSchema(RequestPathParameterSchema): + """project id path parameter schema""" + + project_id = fields.Int( + metadata={"description": "The id of the project"}, + validate=validate.Range(min=1), + required=True + ) + + +class ProjectFirstNationsQueryParamSchema(RequestQueryParameterSchema): + """Project first nations query parameters""" + + work_id = fields.Int( + metadata={"description": "The id of the work"}, + validate=validate.Range(min=1), + required=True + ) + work_type_id = fields.Int( + metadata={"description": "The id of the work type"}, + validate=validate.Range(min=1), + load_default=None, + allow_none=True, + missing=None + ) + + +class ProjectAbbreviationParameterSchema(RequestPathParameterSchema): + """project id path parameter schema""" + + name = fields.Str( + metadata={"description": "Name of project"}, + validate=validate.Length(max=150), + required=True, + ) diff --git a/epictrack-api/src/api/schemas/validators/__init__.py b/epictrack-api/src/api/schemas/validators/__init__.py index f0640cc76..5fee168a7 100644 --- a/epictrack-api/src/api/schemas/validators/__init__.py +++ b/epictrack-api/src/api/schemas/validators/__init__.py @@ -12,4 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. """Exposes custom validators""" +from marshmallow import ValidationError + from .phone import Phone + + +def is_uppercase(value): + """Validates that the value is in uppercase""" + if not value.isupper(): + raise ValidationError("Value must be in uppercase.") diff --git a/epictrack-api/src/api/services/project.py b/epictrack-api/src/api/services/project.py index 9d19e60c8..0e9a6faaa 100644 --- a/epictrack-api/src/api/services/project.py +++ b/epictrack-api/src/api/services/project.py @@ -1,286 +1,320 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Service to manage Project.""" -from typing import IO, List - -import numpy as np -import pandas as pd -from flask import current_app -from sqlalchemy import and_ - -from api.exceptions import ResourceExistsError, ResourceNotFoundError -from api.models import Project, db -from api.models.project import ProjectStateEnum -from api.models.indigenous_nation import IndigenousNation -from api.models.indigenous_work import IndigenousWork -from api.models.proponent import Proponent -from api.models.region import Region -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.utils.constants import PROJECT_STATE_ENUM_MAPS -from api.utils.token_info import TokenInfo - - -class ProjectService: - """Service to manage project related operations.""" - - @classmethod - def find(cls, project_id): - """Find by project id.""" - project = Project.find_by_id(project_id) - return project - - @classmethod - def find_all(cls): - """Find all projects""" - return Project.find_all(default_filters=False) - - @classmethod - def create_project(cls, payload: dict): - """Create a new project.""" - exists = cls.check_existence(payload["name"]) - if exists: - raise ResourceExistsError("Project with same name exists") - project = Project(**payload) - project.project_state = ProjectStateEnum.PRE_WORK - current_app.logger.info(f"Project obj {dir(project)}") - project.save() - return project - - @classmethod - def update_project(cls, project_id: int, payload: dict): - """Update existing project.""" - exists = cls.check_existence(payload["name"], project_id) - if exists: - raise ResourceExistsError("Project with same name exists") - project = Project.find_by_id(project_id) - if not project: - raise ResourceNotFoundError(f"Project with id '{project_id}' not found.") - project = project.update(payload) - return project - - @classmethod - def delete_project(cls, project_id: int): - """Delete project by id.""" - project = Project.find_by_id(project_id) - project.is_deleted = True - project.save() - return True - - @classmethod - def check_existence(cls, name, project_id=None): - """Checks if a project exists with given name""" - return Project.check_existence(name, project_id) - - @classmethod - def find_project_work_types(cls, project_id: int, work_id: int) -> [WorkType]: - """Find all work types associated with the project""" - return ( - db.session.query(WorkType) - .join( - Work, - and_( - Work.work_type_id == WorkType.id, - Work.project_id == project_id, - Work.id != work_id, - ), - ) - .filter( - WorkType.is_active.is_(True), - WorkType.is_deleted.is_(False), - ) - .all() - ) - - @classmethod - def find_first_nations( - cls, project_id: int, work_id: int, work_type_id: int = None - ) -> [IndigenousNation]: - """Find all first nations associated with the project""" - qry = ( - db.session.query(IndigenousNation) - .join( - IndigenousWork, - IndigenousWork.indigenous_nation_id == IndigenousNation.id, - ) - .join( - Work, - and_(Work.id == IndigenousWork.work_id, Work.project_id == project_id), - ) - .filter( - IndigenousNation.is_active.is_(True), - IndigenousNation.is_deleted.is_(False), - Work.id != work_id, - ) - ) - if work_type_id: - qry.filter(Work.work_type_id == work_type_id) - return qry.all() - - @classmethod - def check_first_nation_available(cls, project_id: int, work_id: int) -> bool: - """Checks if any first nation exists for given project""" - result = ( - db.session.query(Project) - .join( - Work, - and_(Work.project_id == project_id, Work.id != work_id), - ) - .join( - IndigenousWork, - Work.id == IndigenousWork.work_id, - ) - .join(IndigenousNation, IndigenousNation.id == IndigenousWork.indigenous_nation_id,) - .filter( - Project.id == Work.project_id, - IndigenousWork.is_active.is_(True), - IndigenousWork.is_deleted.is_(False), - IndigenousNation.is_active.is_(True), - IndigenousNation.is_deleted.is_(False), - Work.is_active.is_(True), - Work.is_deleted.is_(False), - ) - ) - result = result.count() > 0 - return {"first_nation_available": result} - - @classmethod - def import_projects(cls, file: IO): - """Import proponents""" - data = cls._read_excel(file) - proponent_names = set(data["proponent_id"].to_list()) - type_names = set(data["type_id"].to_list()) - sub_type_names = set(data["sub_type_id"].to_list()) - env_region_names = set(data["region_id_env"].to_list()) - flnro_region_names = set(data["region_id_flnro"].to_list()) - proponents = ( - db.session.query(Proponent) - .filter(Proponent.name.in_(proponent_names), Proponent.is_active.is_(True)) - .all() - ) - types = ( - db.session.query(Type) - .filter(Type.name.in_(type_names), Type.is_active.is_(True)) - .all() - ) - sub_types = ( - db.session.query(SubType) - .filter(SubType.name.in_(sub_type_names), SubType.is_active.is_(True)) - .all() - ) - regions = ( - db.session.query(Region) - .filter(Region.name.in_(env_region_names.union(flnro_region_names)), Region.is_active.is_(True)) - .all() - ) - - data["proponent_id"] = data.apply( - lambda x: cls._find_proponent_id(x["proponent_id"], proponents), axis=1 - ) - data["type_id"] = data.apply( - lambda x: cls._find_type_id(x["type_id"], types), axis=1 - ) - data["sub_type_id"] = data.apply( - lambda x: cls._find_sub_type_id(x["sub_type_id"], sub_types), axis=1 - ) - data["region_id_env"] = data.apply( - lambda x: cls._find_region_id(x["region_id_env"], regions, "ENV"), axis=1 - ) - data["region_id_flnro"] = data.apply( - lambda x: cls._find_region_id(x["region_id_flnro"], regions, "FLNR"), axis=1 - ) - data["project_state"] = data.apply( - lambda x: PROJECT_STATE_ENUM_MAPS[x["project_state"]], axis=1 - ) - - username = TokenInfo.get_username() - data["created_by"] = username - data = data.to_dict("records") - db.session.bulk_insert_mappings(Project, data) - db.session.commit() - return "Created successfully" - - @classmethod - def _read_excel(cls, file: IO) -> pd.DataFrame: - """Read the template excel file""" - column_map = { - "Name": "name", - "Proponent": "proponent_id", - "Type": "type_id", - "SubType": "sub_type_id", - "Description": "description", - "Address": "address", - "Latitude": "latitude", - "Longitude": "longitude", - "ENVRegion": "region_id_env", - "FLNRORegion": "region_id_flnro", - "Capital Investment": "capital_investment", - "EPIC Guid": "epic_guid", - "Abbreviation": "abbreviation", - "EACertificate": "ea_certificate", - "Project Closed": "is_project_closed", - "FTE Positions Construction": "fte_positions_construction", - "FTE Positions Operation": "fte_positions_operation", - "Project State": "project_state" - } - data_frame = pd.read_excel(file) - data_frame.rename(column_map, axis="columns", inplace=True) - data_frame = data_frame.infer_objects() - data_frame = data_frame.apply(lambda x: x.str.strip() if x.dtype == "object" else x) - data_frame = data_frame.replace({np.nan: None}) - data_frame = data_frame.replace({np.NaN: None}) - return data_frame - - @classmethod - def _find_proponent_id(cls, name: str, proponents: List[Proponent]) -> int: - """Find and return the id of proponent from given list""" - if name is None: - return None - proponent = next((x for x in proponents if x.name == name), None) - if proponent is None: - print(f"Proponent with name {name} does not exist") - raise ResourceNotFoundError(f"Proponent with name {name} does not exist") - return proponent.id - - @classmethod - def _find_type_id(cls, name: str, types: List[Type]) -> int: - """Find and return the id of type from given list""" - if name is None: - return None - type_obj = next((x for x in types if x.name == name), None) - if type_obj is None: - raise ResourceNotFoundError(f"Type with name {name} does not exist") - return type_obj.id - - @classmethod - def _find_sub_type_id(cls, name: str, sub_types: List[SubType]) -> int: - """Find and return the id of SubType from given list""" - if name is None: - return None - sub_type = next((x for x in sub_types if x.name == name), None) - if sub_type is None: - raise ResourceNotFoundError(f"SubType with name {name} does not exist") - return sub_type.id - - @classmethod - def _find_region_id(cls, name: str, regions: List[Region], entity: str) -> int: - """Find and return the id of region from given list""" - if name is None: - return None - region = next((x for x in regions if x.name == name and x.entity == entity), None) - if region is None: - raise ResourceNotFoundError(f"Region with name {name} does not exist") - return region.id +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Service to manage Project.""" +from typing import IO, List + +import numpy as np +import pandas as pd +from flask import current_app +from sqlalchemy import and_ + +from api.exceptions import BadRequestError, ResourceExistsError, ResourceNotFoundError +from api.models import Project, db +from api.models.indigenous_nation import IndigenousNation +from api.models.indigenous_work import IndigenousWork +from api.models.project import ProjectStateEnum +from api.models.proponent import Proponent +from api.models.region import Region +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.utils.constants import PROJECT_STATE_ENUM_MAPS +from api.utils.enums import ProjectCodeMethod +from api.utils.token_info import TokenInfo + + +class ProjectService: + """Service to manage project related operations.""" + + @classmethod + def find(cls, project_id): + """Find by project id.""" + project = Project.find_by_id(project_id) + return project + + @classmethod + def find_all(cls): + """Find all projects""" + return Project.find_all(default_filters=False) + + @classmethod + def create_project(cls, payload: dict): + """Create a new project.""" + exists = cls.check_existence(payload["name"]) + if exists: + raise ResourceExistsError("Project with same name exists") + project = Project(**payload) + project.project_state = ProjectStateEnum.PRE_WORK + current_app.logger.info(f"Project obj {dir(project)}") + project.save() + return project + + @classmethod + def update_project(cls, project_id: int, payload: dict): + """Update existing project.""" + exists = cls.check_existence(payload["name"], project_id) + if exists: + raise ResourceExistsError("Project with same name exists") + project = Project.find_by_id(project_id) + if not project: + raise ResourceNotFoundError(f"Project with id '{project_id}' not found.") + project = project.update(payload) + return project + + @classmethod + def delete_project(cls, project_id: int): + """Delete project by id.""" + project = Project.find_by_id(project_id) + project.is_deleted = True + project.save() + return True + + @classmethod + def check_existence(cls, name, project_id=None): + """Checks if a project exists with given name""" + return Project.check_existence(name, project_id) + + @classmethod + def find_project_work_types(cls, project_id: int, work_id: int) -> [WorkType]: + """Find all work types associated with the project""" + return ( + db.session.query(WorkType) + .join( + Work, + and_( + Work.work_type_id == WorkType.id, + Work.project_id == project_id, + Work.id != work_id, + ), + ) + .filter( + WorkType.is_active.is_(True), + WorkType.is_deleted.is_(False), + ) + .all() + ) + + @classmethod + def find_first_nations( + cls, project_id: int, work_id: int, work_type_id: int = None + ) -> [IndigenousNation]: + """Find all first nations associated with the project""" + qry = ( + db.session.query(IndigenousNation) + .join( + IndigenousWork, + IndigenousWork.indigenous_nation_id == IndigenousNation.id, + ) + .join( + Work, + and_(Work.id == IndigenousWork.work_id, Work.project_id == project_id), + ) + .filter( + IndigenousNation.is_active.is_(True), + IndigenousNation.is_deleted.is_(False), + Work.id != work_id, + ) + ) + if work_type_id: + qry.filter(Work.work_type_id == work_type_id) + return qry.all() + + @classmethod + def check_first_nation_available(cls, project_id: int, work_id: int) -> bool: + """Checks if any first nation exists for given project""" + result = ( + db.session.query(Project) + .join( + Work, + and_(Work.project_id == project_id, Work.id != work_id), + ) + .join( + IndigenousWork, + Work.id == IndigenousWork.work_id, + ) + .join(IndigenousNation, IndigenousNation.id == IndigenousWork.indigenous_nation_id,) + .filter( + Project.id == Work.project_id, + IndigenousWork.is_active.is_(True), + IndigenousWork.is_deleted.is_(False), + IndigenousNation.is_active.is_(True), + IndigenousNation.is_deleted.is_(False), + Work.is_active.is_(True), + Work.is_deleted.is_(False), + ) + ) + result = result.count() > 0 + return {"first_nation_available": result} + + @classmethod + def import_projects(cls, file: IO): + """Import proponents""" + data = cls._read_excel(file) + proponent_names = set(data["proponent_id"].to_list()) + type_names = set(data["type_id"].to_list()) + sub_type_names = set(data["sub_type_id"].to_list()) + env_region_names = set(data["region_id_env"].to_list()) + flnro_region_names = set(data["region_id_flnro"].to_list()) + proponents = ( + db.session.query(Proponent) + .filter(Proponent.name.in_(proponent_names), Proponent.is_active.is_(True)) + .all() + ) + types = ( + db.session.query(Type) + .filter(Type.name.in_(type_names), Type.is_active.is_(True)) + .all() + ) + sub_types = ( + db.session.query(SubType) + .filter(SubType.name.in_(sub_type_names), SubType.is_active.is_(True)) + .all() + ) + regions = ( + db.session.query(Region) + .filter(Region.name.in_(env_region_names.union(flnro_region_names)), Region.is_active.is_(True)) + .all() + ) + + data["proponent_id"] = data.apply( + lambda x: cls._find_proponent_id(x["proponent_id"], proponents), axis=1 + ) + data["type_id"] = data.apply( + lambda x: cls._find_type_id(x["type_id"], types), axis=1 + ) + data["sub_type_id"] = data.apply( + lambda x: cls._find_sub_type_id(x["sub_type_id"], sub_types), axis=1 + ) + data["region_id_env"] = data.apply( + lambda x: cls._find_region_id(x["region_id_env"], regions, "ENV"), axis=1 + ) + data["region_id_flnro"] = data.apply( + lambda x: cls._find_region_id(x["region_id_flnro"], regions, "FLNR"), axis=1 + ) + data["project_state"] = data.apply( + lambda x: PROJECT_STATE_ENUM_MAPS[x["project_state"]], axis=1 + ) + + username = TokenInfo.get_username() + data["created_by"] = username + data = data.to_dict("records") + db.session.bulk_insert_mappings(Project, data) + db.session.commit() + return "Created successfully" + + @classmethod + def _read_excel(cls, file: IO) -> pd.DataFrame: + """Read the template excel file""" + column_map = { + "Name": "name", + "Proponent": "proponent_id", + "Type": "type_id", + "SubType": "sub_type_id", + "Description": "description", + "Address": "address", + "Latitude": "latitude", + "Longitude": "longitude", + "ENVRegion": "region_id_env", + "FLNRORegion": "region_id_flnro", + "Capital Investment": "capital_investment", + "EPIC Guid": "epic_guid", + "Abbreviation": "abbreviation", + "EACertificate": "ea_certificate", + "Project Closed": "is_project_closed", + "FTE Positions Construction": "fte_positions_construction", + "FTE Positions Operation": "fte_positions_operation", + "Project State": "project_state" + } + data_frame = pd.read_excel(file) + data_frame.rename(column_map, axis="columns", inplace=True) + data_frame = data_frame.infer_objects() + data_frame = data_frame.apply(lambda x: x.str.strip() if x.dtype == "object" else x) + data_frame = data_frame.replace({np.nan: None}) + data_frame = data_frame.replace({np.NaN: None}) + return data_frame + + @classmethod + def _find_proponent_id(cls, name: str, proponents: List[Proponent]) -> int: + """Find and return the id of proponent from given list""" + if name is None: + return None + proponent = next((x for x in proponents if x.name == name), None) + if proponent is None: + print(f"Proponent with name {name} does not exist") + raise ResourceNotFoundError(f"Proponent with name {name} does not exist") + return proponent.id + + @classmethod + def _find_type_id(cls, name: str, types: List[Type]) -> int: + """Find and return the id of type from given list""" + if name is None: + return None + type_obj = next((x for x in types if x.name == name), None) + if type_obj is None: + raise ResourceNotFoundError(f"Type with name {name} does not exist") + return type_obj.id + + @classmethod + def _find_sub_type_id(cls, name: str, sub_types: List[SubType]) -> int: + """Find and return the id of SubType from given list""" + if name is None: + return None + sub_type = next((x for x in sub_types if x.name == name), None) + if sub_type is None: + raise ResourceNotFoundError(f"SubType with name {name} does not exist") + return sub_type.id + + @classmethod + def _find_region_id(cls, name: str, regions: List[Region], entity: str) -> int: + """Find and return the id of region from given list""" + if name is None: + return None + region = next((x for x in regions if x.name == name and x.entity == entity), None) + if region is None: + raise ResourceNotFoundError(f"Region with name {name} does not exist") + return region.id + + @classmethod + def _generate_project_abbreviation(cls, project_name: str, method: ProjectCodeMethod): + words = project_name.split() + + # Method 1: 1st 3 LETTERS OF FIRST WORD IN NAME + FIRST 3 LETTERS OF 2nd WORD IN NAME + if method == ProjectCodeMethod.METHOD_1 and len(words) >= 2: + return f'{words[0][:3]}{words[1][:3]}'.upper() + + # Method 2: 1st LETTER OF FIRST WORD IN NAME + # + 1st LETTER OF 2nd WORD IN NAME + 1st FOUR LETTERS OF THIRD WORD IN NAME + if method == ProjectCodeMethod.METHOD_2 and len(words) >= 3: + return f'{words[0][0]}{words[1][0]}{words[2][:4]}'.upper() + + # Method 3: 1st 6 LETTERS OF FIRST WORD IN NAME + if method == ProjectCodeMethod.METHOD_3 and len(words[0]) >= 6: + return words[0][:6].upper() + + return None + + @classmethod + def create_project_abbreviation(cls, project_name: str): + """Return a project code based on the project name""" + for method in ProjectCodeMethod: + project_abbreviation = cls._generate_project_abbreviation(project_name, method) + + if project_abbreviation is not None: + # Check if project abbreviation already exists + project = Project.get_by_abbreviation(project_abbreviation) + if not project: + return project_abbreviation + + raise BadRequestError("Could not generate a unique project abbreviation") diff --git a/epictrack-api/src/api/utils/enums.py b/epictrack-api/src/api/utils/enums.py index c1cb23b33..3a3bfa8bc 100644 --- a/epictrack-api/src/api/utils/enums.py +++ b/epictrack-api/src/api/utils/enums.py @@ -23,3 +23,11 @@ class HttpMethod(Enum): POST = 'POST' PATCH = 'PATCH' DELETE = 'DELETE' + + +class ProjectCodeMethod(Enum): + """Project abbreviation code generation methods""" + + METHOD_1 = 1 + METHOD_2 = 2 + METHOD_3 = 3 diff --git a/epictrack-web/src/components/project/ProjectForm.tsx b/epictrack-web/src/components/project/ProjectForm.tsx index 5cdf2dbe5..f57cc881c 100644 --- a/epictrack-web/src/components/project/ProjectForm.tsx +++ b/epictrack-web/src/components/project/ProjectForm.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { TextField, Grid, Box } from "@mui/material"; +import { Grid, Box } from "@mui/material"; import { FormProvider, useForm, useWatch } from "react-hook-form"; import * as yup from "yup"; import { yupResolver } from "@hookform/resolvers/yup"; @@ -17,6 +17,7 @@ import projectService from "../../services/projectService/projectService"; import LockClosed from "../../assets/images/lock-closed.svg"; import ControlledSwitch from "../shared/controlledInputComponents/ControlledSwitch"; import { Palette } from "../../styles/theme"; +import ControlledTextField from "../shared/controlledInputComponents/ControlledTextField"; const schema = yup.object().shape({ name: yup @@ -87,6 +88,8 @@ export default function ProjectForm({ ...props }) { formState: { errors }, reset, setValue, + setError, + resetField, } = methods; const formValues = useWatch({ control }); @@ -157,6 +160,29 @@ export default function ProjectForm({ ...props }) { reset(); }); }; + + const onBlurProjectName = async () => { + if (!formValues.name || Boolean(formValues.abbreviation)) return; + + try { + const response = await projectService.createProjectAbbreviation( + formValues.name + ); + const generatedAbbreviation = response.data as string; + resetField("abbreviation"); + setValue("abbreviation", generatedAbbreviation); + } catch (error) { + if (formValues.abbreviation) { + return; + } + + setError("abbreviation", { + type: "manual", + message: `Abbreviation could not be auto-generated for "${formValues.name}"`, + }); + } + }; + return ( <> @@ -200,14 +226,13 @@ export default function ProjectForm({ ...props }) { /> - @@ -272,13 +297,11 @@ export default function ProjectForm({ ...props }) { Project Description - @@ -295,42 +318,36 @@ export default function ProjectForm({ ...props }) { > Location Description - Latitude - Longitude - @@ -372,66 +389,56 @@ export default function ProjectForm({ ...props }) { > Capital Investment - EPIC GUID - + Est. FTE Positions in Construction - Est. FTE Positions in Operation - Certificate Number - + Abbreviation - e.target.value.toUpperCase()} /> = ({ name, ...otherProps }) => { - const { - control, - formState: { errors, defaultValues }, - } = useFormContext(); - - return ( - ( - - )} - /> - ); -}; - -export default ControlledTextField; +import React, { FC } from "react"; +import { TextField, TextFieldProps } from "@mui/material"; +import { Controller, useFormContext } from "react-hook-form"; + +type IFormInputProps = { + name: string; + inputEffects?: ( + e: React.ChangeEvent + ) => string; +} & TextFieldProps; + +const ControlledTextField: FC = ({ + name, + inputEffects, + ...otherProps +}) => { + const { + control, + formState: { errors, defaultValues }, + } = useFormContext(); + + return ( + ( + { + if (inputEffects) { + e.target.value = inputEffects(e); + } + field.onChange(e.target.value); + }} + error={!!errors[name]} + helperText={String(errors[name]?.message || "")} + /> + )} + /> + ); +}; + +export default ControlledTextField; diff --git a/epictrack-web/src/constants/api-endpoint.ts b/epictrack-web/src/constants/api-endpoint.ts index 5413cb4e6..f332e89bb 100644 --- a/epictrack-web/src/constants/api-endpoint.ts +++ b/epictrack-web/src/constants/api-endpoint.ts @@ -12,6 +12,7 @@ const Endpoints = { WORK_TYPES: "projects/:project_id/work-types", FIRST_NATIONS: "projects/:project_id/first-nations", FIRST_NATION_AVAILABLE: "projects/:project_id/first-nation-available", + PROJECT_ABBREVIATION: "projects/abbreviation", }, Codes: { GET_CODES: "codes", diff --git a/epictrack-web/src/services/projectService/projectService.ts b/epictrack-web/src/services/projectService/projectService.ts index 7fb994fa9..45b2f8efe 100644 --- a/epictrack-web/src/services/projectService/projectService.ts +++ b/epictrack-web/src/services/projectService/projectService.ts @@ -64,6 +64,15 @@ class ProjectService implements ServiceBase { ); return await http.GetRequest(url, { work_id }); } + + async createProjectAbbreviation(name: string) { + return await http.PostRequest( + Endpoints.Projects.PROJECT_ABBREVIATION, + JSON.stringify({ + name, + }) + ); + } } export default new ProjectService();