Skip to content

Commit

Permalink
Update team members when EPD and Work Load change in Work Form (#2155)
Browse files Browse the repository at this point in the history
* update team members list in work after special field operation

* Hide team members in form and disable edit in list

* Add missing docstring

* Remove prints and unnecessary change

* Fix failing unit tests

* Fix linting issues

* fix linting issue

* Add migration to add staff role
  • Loading branch information
jadmsaadaot authored Apr 23, 2024
1 parent 613cff1 commit 72c9170
Show file tree
Hide file tree
Showing 16 changed files with 207 additions and 50 deletions.
38 changes: 38 additions & 0 deletions epictrack-api/migrations/versions/c64aaa268112_add_staff_role.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Add team co-lead to staff roles
Revision ID: c64aaa268112
Revises: a67589eb0b5f
Create Date: 2024-04-22 16:25:53.831382
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'c64aaa268112'
down_revision = 'a67589eb0b5f'
branch_labels = None
depends_on = None
team_co_lead = 'Team Co-Lead'

def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
roles = sa.Table(
'roles',
sa.MetaData(),
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('name', sa.String, nullable=False),
sa.Column('sort_order', sa.Integer, nullable=False),
)

op.bulk_insert(roles, [
{'name': team_co_lead, 'sort_order': 5},
])
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.execute(f"DELETE FROM roles WHERE name = '{team_co_lead}'")
# ### end Alembic commands ###
10 changes: 10 additions & 0 deletions epictrack-api/src/api/models/role.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,13 @@ def as_dict(self):
'id': self.id,
'name': self.name
}

@classmethod
def find_by_name(cls, name):
"""Find role by name."""
return cls.query.filter_by(name=name).one_or_none()

@classmethod
def find_all_by_names(cls, names):
"""Find role by name."""
return cls.query.filter(Role.name.in_(names)).all()
1 change: 1 addition & 0 deletions epictrack-api/src/api/models/special_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,5 @@ def find_by_params(cls, params: dict, default_filters=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
35 changes: 35 additions & 0 deletions epictrack-api/src/api/models/staff_work_role.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ def find_by_role_and_work(cls, work_id: int, role_id):
StaffWorkRole.is_active.is_(True),
).all()

@classmethod
def find_one_by_role_and_work(cls, work_id: int, role_id):
"""Return by work and role ids."""
return cls.query.filter(
StaffWorkRole.work_id == work_id,
StaffWorkRole.role_id == role_id,
StaffWorkRole.is_deleted.is_(False),
StaffWorkRole.is_active.is_(True),
).first()

@classmethod
def find_by_work_and_staff_and_role(cls, work_id: int, staff_id: object, role_id: object, work_staff_id: object):
"""Return by work, staff and role ids."""
Expand All @@ -72,3 +82,28 @@ def find_by_work_and_staff_and_role(cls, work_id: int, staff_id: object, role_id
query = query.filter(StaffWorkRole.id != work_staff_id)

return query.all()

@classmethod
def find_one_by_work_and_staff_and_role(
cls,
work_id: int,
staff_id: object,
role_id: object,
work_staff_id: object,
is_active: bool = None
):
"""Return by work, staff and role ids."""
query = cls.query.filter(
StaffWorkRole.work_id == work_id,
StaffWorkRole.staff_id == staff_id,
StaffWorkRole.is_deleted.is_(False),
StaffWorkRole.role_id == role_id,
)

if is_active is not None:
query = query.filter(StaffWorkRole.is_active.is_(is_active))

if work_staff_id:
query = query.filter(StaffWorkRole.id != work_staff_id)

return query.first()
1 change: 0 additions & 1 deletion epictrack-api/src/api/resources/work.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,6 @@ def get(work_phase_id):
"""Get the status if template upload is available"""
req.WorkIdPhaseIdPathParameterSchema().load(request.view_args)
work_phase = WorkPhase.find_by_id(work_phase_id)
print(work_phase.name)
return (
res.WorkPhaseByIdResponseSchema().dump({'work_phase': work_phase}),
HTTPStatus.OK,
Expand Down
14 changes: 14 additions & 0 deletions epictrack-api/src/api/services/role.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,17 @@ def find_all(cls):
current_app.logger.debug("find roles")
roles = Role.find_all()
return roles

@classmethod
def find_by_name(cls, name):
"""Find role by name"""
current_app.logger.debug("find role by name")
role = Role.find_by_name(name)
return role

@classmethod
def find_all_by_names(cls, names):
"""Find role by name"""
current_app.logger.debug("find roles by names")
roles = Role.find_all_by_names(names)
return roles
30 changes: 30 additions & 0 deletions epictrack-api/src/api/services/special_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from api.exceptions import ResourceNotFoundError
from api.models import SpecialField, db
from api.models.role import RoleEnum
from api.models.special_field import EntityEnum
from api.utils.constants import SPECIAL_FIELD_ENTITY_MODEL_MAPS

Expand Down Expand Up @@ -115,9 +116,38 @@ def _update_original_model(cls, special_field_entry: SpecialField) -> None:
model_class = SPECIAL_FIELD_ENTITY_MODEL_MAPS[
EntityEnum(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})
cls.run_other_related_updates(special_field_entry)

@classmethod
def run_other_related_updates(cls, special_field: SpecialField):
"""Run other related updates based on special field entry."""
from api.services.work import WorkService # pylint: disable=import-outside-toplevel
special_field_entity = EntityEnum(special_field.entity).value
if special_field_entity == EntityEnum.WORK.value:
data = {
"staff_id": special_field.field_value,
"is_active": True
}
if special_field.field_name == "responsible_epd_id":
data = {
**data,
"role_id": RoleEnum.RESPONSIBLE_EPD.value
}
WorkService.replace_work_staff(
special_field.entity_id, data
)
if special_field.field_name == "work_lead_id":
data = {
**data,
"role_id": RoleEnum.TEAM_LEAD.value
}
WorkService.replace_work_staff(
special_field.entity_id, data
)

@classmethod
def find_special_history_by_date_range(
Expand Down
46 changes: 29 additions & 17 deletions epictrack-api/src/api/services/work.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,11 @@
StaffWorkRoleResponseSchema,
WorkPhaseAdditionalInfoResponseSchema,
WorkResponseSchema,
WorkStatusResponseSchema,
)
WorkStatusResponseSchema,)
from api.schemas.work_first_nation import WorkFirstNationSchema
from api.schemas.work_plan import WorkPlanSchema
from api.schemas.work_type import WorkTypeSchema
from api.services import authorisation
from api.services.code import CodeService
from api.services.event import EventService
from api.services.event_template import EventTemplateService
from api.services.outcome_configuration import OutcomeConfigurationService
Expand Down Expand Up @@ -263,19 +261,7 @@ def create_work(cls, payload, commit: bool = True):
work.current_work_phase_id = work_phase.id
sort_order = sort_order + 1
# dev-note: find_code_values_by_type - we should use RoleService instead of the "code" way
role_id = (
CodeService.find_code_values_by_type("roles", {"name": "Team Lead"})
.get("codes")[0]
.get("id")
)
WorkService.create_work_staff(
work.id,
{
"staff_id": payload["work_lead_id"],
"role_id": role_id,
"is_active": True,
},
)

if commit:
db.session.commit()
return work
Expand Down Expand Up @@ -443,6 +429,33 @@ def update_work_staff(
db.session.commit()
return work_staff

@classmethod
def upsert_work_staff(cls, work_id: int, data: dict, commit: bool = True):
"""Upsert work staff"""
work_staff = StaffWorkRole.find_one_by_work_and_staff_and_role(
work_id, data.get("staff_id"), data.get("role_id"), work_staff_id=None)
if work_staff:
return cls.update_work_staff(work_staff.id, data, commit)
return cls.create_work_staff(work_id, data, commit)

@classmethod
def replace_work_staff(cls, work_id: int, data: dict):
"""Replace work staff"""
work_staff = StaffWorkRole.find_one_by_role_and_work(
work_id, data.get("role_id")
)
if work_staff:
update_data = {
"role_id": work_staff.role_id,
"staff_id": work_staff.staff_id,
"is_active": False,
}
cls.update_work_staff(work_staff.id, update_data, commit=False)

upserted_work_staff = cls.upsert_work_staff(work_id, data, commit=False)
db.session.commit()
return upserted_work_staff

@classmethod
def copy_outcome_and_actions(
cls, template: dict, config: EventConfiguration, from_template: bool = True
Expand Down Expand Up @@ -740,7 +753,6 @@ def generate_first_nations_excel(
"""Generate the workplan excel file for given work and phase"""
cls._check_can_edit_or_team_member_auth(work_id)
first_nations = cls.find_first_nations(work_id, None)
print(first_nations)
schema = WorkFirstNationSchema(many=True)
data = schema.dump(first_nations)

Expand Down
1 change: 0 additions & 1 deletion epictrack-api/tests/unit/apis/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ def _change_event_anticipated_date(
url = urljoin(API_BASE_URL, "works")
work_response = client.post(url, json=work_data, headers=headers)
work_response_json = work_response.json
print(work_response_json)
work_id = work_response_json["id"]
work_phases = WorkPhase.find_by_params({"work_id": work_id})
# first work phase in the assessment would be non-legislated and second is legislated
Expand Down
8 changes: 3 additions & 5 deletions epictrack-api/tests/unit/apis/test_insights.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,12 @@ def test_get_works_by_staff(client, auth_header):
url = urljoin(API_BASE_URL, "works")
work_response = client.post(url, json=payload, headers=auth_header)
work_response_json = work_response.json
print(work_response_json)
url = urljoin(API_BASE_URL, "insights/works?group_by=staff")
result = client.get(url, headers=auth_header)
assert result.status_code == HTTPStatus.OK
assert len(result.json) == 1
staff_insight = result.json[0]
assert staff_insight["staff_id"] == work_response_json["work_lead_id"]
assert staff_insight["count"] == 1
assert len(result.json) == 2
initial_staff = [work_response_json["work_lead_id"], work_response_json["responsible_epd_id"]]
assert set([staff_insight["staff_id"] for staff_insight in result.json]) == set(initial_staff)


def test_get_works_by_ministry(client, auth_header):
Expand Down
10 changes: 4 additions & 6 deletions epictrack-api/tests/unit/apis/test_special_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from tests.utilities.factory_utils import (
factory_proponent_model,
factory_special_field_model,
factory_staff_model,
factory_staff_model, factory_work_model,
)


Expand Down Expand Up @@ -52,10 +52,8 @@ def test_create_special_field(client, auth_header):
payload["entity_id"] = entity_obj.id

# Scenario 1: Valid payload
print(payload)
response = client.post(url, json=payload, headers=auth_header)
response_json = response.json
print(response_json)
assert response.status_code == HTTPStatus.CREATED
assert "id" in response_json
assert payload["entity_id"] == response_json["entity_id"]
Expand Down Expand Up @@ -115,18 +113,19 @@ def test_update_special_field(client, auth_header):

def test_add_special_field_with_upper_limit(client, auth_header):
"""Test update special field"""
work = factory_work_model()
staff1 = factory_staff_model()
staff2 = factory_staff_model()
first_special_field = TestSpecialField.work_entity.value
first_special_field["entity_id"] = 1
first_special_field["entity_id"] = work.id
first_special_field["field_value"] = str(staff1.id)
first_special_field["active_from"] = datetime.fromisoformat(
"2024-01-05T00:00:00-08:00"
).strftime("%Y-%m-%dT%H:%M:%S%z")
special_field = factory_special_field_model(first_special_field)

second_special_field = copy(TestSpecialField.work_entity.value)
second_special_field["entity_id"] = 1
second_special_field["entity_id"] = work.id
second_special_field["field_value"] = str(staff2.id)
second_special_field["active_from"] = datetime.fromisoformat(
"2024-02-05T00:00:00-08:00"
Expand All @@ -138,7 +137,6 @@ def test_add_special_field_with_upper_limit(client, auth_header):
url = urljoin(API_BASE_URL, f"special-fields/{special_field.id}")
response = client.get(url, headers=auth_header)
response_json = response.json
print(response_json)
assert (
datetime.strptime(response_json["active_from"], "%Y-%m-%dT%H:%M:%S%z").date()
- datetime.strptime(
Expand Down
11 changes: 6 additions & 5 deletions epictrack-api/tests/unit/apis/test_works.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,10 +275,11 @@ def test_work_resources(client, auth_header):
== work_response_json["responsible_epd_id"]
)
assert work_resource_json["work_lead"]["id"] == work_response_json["work_lead_id"]
assert len(work_resource_json["staff"]) == 1
work_staff = work_resource_json["staff"][0]
assert work_staff["id"] == work_response_json["work_lead_id"]
assert work_staff["role"]["name"] == "Team Lead"
assert len(work_resource_json["staff"]) == 2
team_lead = [x for x in work_resource_json["staff"] if x["role"]['name'] == 'Team Lead'][0]
epd = [x for x in work_resource_json["staff"] if x["role"]['name'] == 'Responsible EPD'][0]
assert team_lead["id"] == work_response_json["work_lead_id"]
assert epd["id"] == work_response_json["responsible_epd_id"]


def test_get_work_staff_roles(client, auth_header):
Expand All @@ -294,7 +295,7 @@ def test_get_work_staff_roles(client, auth_header):
url = urljoin(API_BASE_URL, f'works/{work_response_json["id"]}/staff-roles')
response = client.get(url, headers=auth_header)
assert response.status_code == HTTPStatus.OK
assert len(response.json) == 1
assert len(response.json) == 2


def test_create_work_staff_roles(client, auth_header):
Expand Down
6 changes: 6 additions & 0 deletions epictrack-web/src/components/shared/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ interface HeaderProps {
interface LinkHeaderProps extends HeaderProps {
to: string | Partial<Path>;
onClick?: (eventArg?: any) => void;
disabled?: boolean;
}

interface PageContainerProps {
Expand Down Expand Up @@ -268,8 +269,13 @@ export const ETGridTitle = ({
color,
children,
sx,
disabled = false,
...rest
}: LinkHeaderProps) => {
if (disabled) {
return <ETParagraph bold={bold}>{children}</ETParagraph>;
}

return (
<ETLink onClick={rest.onClick} {...rest}>
<Tooltip
Expand Down
6 changes: 5 additions & 1 deletion epictrack-web/src/components/workPlan/team/TeamForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { WorkplanContext } from "../WorkPlanContext";
import { getErrorMessage } from "../../../utils/axiosUtils";
import { COMMON_ERROR_MESSAGE } from "../../../constants/application-constant";
import roleService from "services/roleService";
import { unEditableTeamMembers } from "./constants";

interface TeamFormProps {
workStaffId?: number;
Expand Down Expand Up @@ -112,7 +113,10 @@ const TeamForm = ({ onSave, workStaffId }: TeamFormProps) => {
const result = await roleService.getAll();
if (result.status === 200) {
const roles = result.data as ListType[];
setRoles(sort(roles, "name"));
const filteredRoles = roles.filter(
(role) => !unEditableTeamMembers.includes(role.id)
);
setRoles(sort(filteredRoles, "name"));
}
} catch (e) {
showNotification(COMMON_ERROR_MESSAGE, {
Expand Down
Loading

0 comments on commit 72c9170

Please sign in to comment.