From 44a7cb8e46668a8333c791306867e729dc98c9e1 Mon Sep 17 00:00:00 2001 From: "Steven (Quoc)" Date: Sat, 21 Sep 2024 18:48:09 +1000 Subject: [PATCH 1/9] linked roles to specific shifst that require them, will need roles to be generated for each new shift on shift creation. linked the volunteers to the specifc roles in adition to being linked to the shift table edited calculator so that files will be called properly. --- domain/entity/role.py | 3 +- domain/entity/shift_request_volunteer.py | 1 + repository/shift_requirements.py | 21 ++++++ services/optimiser/calculator.py | 83 ++++++++++++++---------- 4 files changed, 73 insertions(+), 35 deletions(-) create mode 100644 repository/shift_requirements.py diff --git a/domain/entity/role.py b/domain/entity/role.py index 0db68070..3c1c5fc5 100644 --- a/domain/entity/role.py +++ b/domain/entity/role.py @@ -1,11 +1,12 @@ from datetime import datetime -from sqlalchemy import Column, Integer, String, DateTime, Boolean +from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Enum from domain.base import Base class Role(Base): __tablename__ = 'role' id = Column(Integer, primary_key=True, autoincrement=True) + shift_request_id = Column(Integer, ForeignKey('shift_request.id'), name='shift_request_id', nullable=False) code = Column(String(256), nullable=False) name = Column(String(256), nullable=False) deleted = Column(Boolean, nullable=False, default=False) diff --git a/domain/entity/shift_request_volunteer.py b/domain/entity/shift_request_volunteer.py index f7008ff8..ca63a323 100644 --- a/domain/entity/shift_request_volunteer.py +++ b/domain/entity/shift_request_volunteer.py @@ -13,6 +13,7 @@ class ShiftRequestVolunteer(Base): id = Column(Integer, primary_key=True, autoincrement=True) user_id = Column(Integer, ForeignKey('user.id'), name='user_id', nullable=False) request_id = Column(Integer, ForeignKey('shift_request.id'), name='request_id', nullable=False) + role_id = Column(Integer, ForeignKey('role.id'), name="role_id", nullable=False) status = Column(Enum(ShiftVolunteerStatus), name='status', nullable=False, default=ShiftVolunteerStatus.PENDING) update_date_time = Column(DateTime, name='last_update_datetime', default=datetime.now(), nullable=False) insert_date_time = Column(DateTime, name='created_datetime', default=datetime.now(), nullable=False) diff --git a/repository/shift_requirements.py b/repository/shift_requirements.py new file mode 100644 index 00000000..3a51f779 --- /dev/null +++ b/repository/shift_requirements.py @@ -0,0 +1,21 @@ +from datetime import datetime + +from sqlalchemy import Column, String, DateTime, ForeignKey, Integer, Enum +from sqlalchemy.orm import relationship + +from domain import ShiftStatus +from domain.base import Base + +class ShiftRequirements(Base): + __tablename__ = 'shift_requirements' + + id = Column(Integer, primary_key=True, autoincrement=True) + request_id = Column(Integer, ForeignKey('shift_request.id'), name='request_id', nullable=False) + role_id = Column(Integer, ForeignKey('role.id'), name='role.id') + + # title = Column(String(29), name='title', nullable=False) + # startTime = Column(DateTime, name='from', nullable=False) + # endTime = Column(DateTime, name='to', nullable=False) + # status = Column(Enum(ShiftStatus), name='status', default=ShiftStatus.WAITING, nullable=False) + # update_date_time = Column(DateTime, name='last_update_datetime', default=datetime.now(), nullable=False) + # insert_date_time = Column(DateTime, name='created_datetime', default=datetime.now(), nullable=False) \ No newline at end of file diff --git a/services/optimiser/calculator.py b/services/optimiser/calculator.py index 1d551a58..14be9011 100644 --- a/services/optimiser/calculator.py +++ b/services/optimiser/calculator.py @@ -3,7 +3,7 @@ from typing import List from sqlalchemy import orm, func, alias -from domain import User, AssetRequestVehicle, AssetType, Role, UserRole, AssetTypeRole +from domain import User, AssetRequestVehicle, AssetType, Role, UserRole, AssetTypeRole, ShiftRequest, ShiftRequestVolunteer, UnavailabilityTime class Calculator: @@ -15,10 +15,10 @@ class Calculator: # Master list of all volunteers, these fetched once so that the order of the records in the list is deterministic. # This matters as the lists passed to Minizinc are not keyed and are instead used by index. _users_ = [] - _asset_request_vehicles_ = [] - _asset_types_ = [] + _shift_ = [] + _roles_ = [] - _asset_type_seats_ = [] + # A single database session is used for all transactions in the optimiser. This is initialised by the calling # function. @@ -55,6 +55,11 @@ def get_number_of_vehicles(self) -> int: @return: The number of vehicles to be optimised. """ return len(self._asset_request_vehicles_) + ## vehicle_count = self._asset_request_vehicles_.filter( asset_request_vehicles.vheicle_id == self.request_id, + ## asset_request_vehicles.vehicle_id == self.vehicle_id # Compare the vehicle ID + ## ).count() + + return vehicle_count def get_number_of_roles(self): """ @@ -74,11 +79,11 @@ def get_volunteer_by_index(self, index) -> User: def get_role_by_index(self, index) -> Role: return self._roles_[index] - def get_asset_request_by_index(self, index) -> AssetRequestVehicle: - return self._asset_request_vehicles_[index] - - def get_asset_requests(self) -> List[AssetRequestVehicle]: - return self._asset_request_vehicles_ + # def get_asset_request_by_index(self, index) -> AssetRequestVehicle: + # return self._asset_request_vehicles_[index] + # + # def get_asset_requests(self) -> List[AssetRequestVehicle]: + # return self._asset_request_vehicles_ def get_roles(self) -> List[Role]: return self._roles_ @@ -91,19 +96,25 @@ def __get_request_data(self): """ self._users_ = self._session_.query(User) \ .all() - self._asset_request_vehicles_ = self._session_.query(AssetRequestVehicle) \ - .filter(AssetRequestVehicle.request_id == self.request_id) \ - .all() - self._asset_types_ = self._session_.query(AssetType) \ - .filter(AssetType.deleted == False) \ - .all() + self._shift_ = self._session_.query(ShiftRequest.id == self.request_id) + + # self._asset_request_vehicles_ = self._session_.query(AssetRequestVehicle) \ + # .filter(AssetRequestVehicle.request_id == self.request_id) \ + # .all() + # self._asset_types_ = self._session_.query(AssetType) \ + # .filter(AssetType.deleted == False) \ + # .all() + + # return the roles that have not been deleted for the specific shift self._roles_ = self._session_.query(Role) \ .filter(Role.deleted == False) \ + .filter(Role.shift_request_id == self.request_id) \ .all() - self._asset_type_seats_ = self._session_.query(AssetTypeRole) \ - .join(Role, Role.id == AssetTypeRole.role_id) \ - .filter(Role.deleted == False) \ - .all() + + # self._asset_type_seats_ = self._session_.query(AssetTypeRole) \ + # .join(Role, Role.id == AssetTypeRole.role_id) \ + # .filter(Role.deleted == False) \ + # .all() def calculate_deltas(self, start: datetime, end: datetime) -> List[datetime]: """ @@ -120,22 +131,26 @@ def calculate_deltas(self, start: datetime, end: datetime) -> List[datetime]: curr += self._time_granularity_ return deltas - @staticmethod - def float_time_to_datetime(float_hours: float, d: datetime) -> datetime: - """ - Given a users available time as a date agnostic decimal hour and a shift blocks date, combine the two into a - datetime that can be used for equality and range comparisons. - @param float_hours: The users decimal hour availability, i.e. 3.5 is 3:30am, 4.0 is 4am, - @param d: The shift blocks date time - @return: The decimal hours time on the shift blocks day as datetime - """ - # Assertion to ensure the front end garbage hasn't continued - assert 0 <= float_hours <= 23.5 + # @staticmethod + # def float_time_to_datetime(float_hours: float, d: datetime) -> datetime: + # """ + # Given a users available time as a date agnostic decimal hour and a shift blocks date, combine the two into a + # datetime that can be used for equality and range comparisons. + # @param float_hours: The users decimal hour availability, i.e. 3.5 is 3:30am, 4.0 is 4am, + # @param d: The shift blocks date time + # @return: The decimal hours time on the shift blocks day as datetime + # """ + # # Assertion to ensure the front end garbage hasn't continued + # assert 0 <= float_hours <= 23.5 + # + # # Calculate the actual datetime + # hours = int(float_hours) + # minutes = int((float_hours * 60) % 60) + # return datetime(d.year, d.month, d.day, hours, minutes, 0) - # Calculate the actual datetime - hours = int(float_hours) - minutes = int((float_hours * 60) % 60) - return datetime(d.year, d.month, d.day, hours, minutes, 0) + """ + original method/s has structure of double, made redundant as times from unavailability_time table is already in datetime format. + """ def calculate_compatibility(self) -> List[List[bool]]: """ From 84d26b55778595dc58a192e50f5c399f8e16c760 Mon Sep 17 00:00:00 2001 From: "Steven (Quoc)" Date: Mon, 23 Sep 2024 01:56:58 +1000 Subject: [PATCH 2/9] modifed the compatibilies and get skill requirements methods --- domain/entity/shift_request.py | 2 +- domain/entity/shift_request_volunteer.py | 2 +- domain/entity/user.py | 1 + services/optimiser/calculator.py | 162 +++++++++++++---------- 4 files changed, 95 insertions(+), 72 deletions(-) diff --git a/domain/entity/shift_request.py b/domain/entity/shift_request.py index e1be64b2..ca5aebce 100644 --- a/domain/entity/shift_request.py +++ b/domain/entity/shift_request.py @@ -19,5 +19,5 @@ class ShiftRequest(Base): status = Column(Enum(ShiftStatus), name='status', default=ShiftStatus.WAITING, nullable=False) update_date_time = Column(DateTime, name='last_update_datetime', default=datetime.now(), nullable=False) insert_date_time = Column(DateTime, name='created_datetime', default=datetime.now(), nullable=False) - + Column() user = relationship("User") \ No newline at end of file diff --git a/domain/entity/shift_request_volunteer.py b/domain/entity/shift_request_volunteer.py index ca63a323..b73447f0 100644 --- a/domain/entity/shift_request_volunteer.py +++ b/domain/entity/shift_request_volunteer.py @@ -13,7 +13,7 @@ class ShiftRequestVolunteer(Base): id = Column(Integer, primary_key=True, autoincrement=True) user_id = Column(Integer, ForeignKey('user.id'), name='user_id', nullable=False) request_id = Column(Integer, ForeignKey('shift_request.id'), name='request_id', nullable=False) - role_id = Column(Integer, ForeignKey('role.id'), name="role_id", nullable=False) + position_id = Column(Integer, ForeignKey('role.id'), name="role_id", nullable=False) status = Column(Enum(ShiftVolunteerStatus), name='status', nullable=False, default=ShiftVolunteerStatus.PENDING) update_date_time = Column(DateTime, name='last_update_datetime', default=datetime.now(), nullable=False) insert_date_time = Column(DateTime, name='created_datetime', default=datetime.now(), nullable=False) diff --git a/domain/entity/user.py b/domain/entity/user.py index ac575b7c..81bd678f 100644 --- a/domain/entity/user.py +++ b/domain/entity/user.py @@ -23,6 +23,7 @@ class User(Base): experience_years = Column(Integer, name='experience_years') possibleRoles = Column(JSON, name='possible_roles') qualifications = Column(JSON, name='qualifications') + unavailability = Column(JSON, name='unavailability') availabilities = Column(JSON, name='availabilities') update_date_time = Column(DateTime, name='last_update_datetime', default=datetime.now(), nullable=False) insert_date_time = Column(DateTime, name='created_datetime', default=datetime.now(), nullable=False) diff --git a/services/optimiser/calculator.py b/services/optimiser/calculator.py index 14be9011..ac08d25a 100644 --- a/services/optimiser/calculator.py +++ b/services/optimiser/calculator.py @@ -15,7 +15,7 @@ class Calculator: # Master list of all volunteers, these fetched once so that the order of the records in the list is deterministic. # This matters as the lists passed to Minizinc are not keyed and are instead used by index. _users_ = [] - _shift_ = [] + _shifts_ = [] _roles_ = [] @@ -96,7 +96,8 @@ def __get_request_data(self): """ self._users_ = self._session_.query(User) \ .all() - self._shift_ = self._session_.query(ShiftRequest.id == self.request_id) + self._shift_ = self._session_.query(ShiftRequest) \ + .all() # self._asset_request_vehicles_ = self._session_.query(AssetRequestVehicle) \ # .filter(AssetRequestVehicle.request_id == self.request_id) \ @@ -105,10 +106,9 @@ def __get_request_data(self): # .filter(AssetType.deleted == False) \ # .all() - # return the roles that have not been deleted for the specific shift + # return the roles that have not been deleted for the all shifts self._roles_ = self._session_.query(Role) \ .filter(Role.deleted == False) \ - .filter(Role.shift_request_id == self.request_id) \ .all() # self._asset_type_seats_ = self._session_.query(AssetTypeRole) \ @@ -148,99 +148,121 @@ def calculate_deltas(self, start: datetime, end: datetime) -> List[datetime]: # minutes = int((float_hours * 60) % 60) # return datetime(d.year, d.month, d.day, hours, minutes, 0) + def calculate_compatibility(self) -> List[bool]: + compatibilities = [] + # Shift blocks are the _time_granularity_ sections the volunteer would need to be available for. + # It's calculated by finding all the 30 minute slots between the start time and end time (inclusive) + shift_block = self.calculate_deltas(self._shift_.startTime, self._shift_.endTime) + for user in self._users_: + available = True # assume volunteer is available + # first pull then calculate the unavailability block of the user and check if it overlaps with the shift block + unavailabilities = self._session_.query(UnavailabilityTime) \ + .filter(UnavailabilityTime.userId == user.id)\ + .all() + for unavailability in unavailabilities: + + + + + + + """ original method/s has structure of double, made redundant as times from unavailability_time table is already in datetime format. """ def calculate_compatibility(self) -> List[List[bool]]: """ - Generates a 2D array of compatibilities between volunteers availabilities and the requirements of the shift. - This is the fairly critical function of the optimiser as its determining in a simple manner if a user is - even available for assignment, regardless of role. - - Example 1: The volunteer is available between 2pm to 3pm and the shift is from 2pm to 2:30pm: - Result: True - Example 2: The volunteer is available from 1pm to 2pm and the shift is from 1pm to 3pm: - Result: False - @return: + Generates a 2D array of compatibilities between volunteers' unavailability and the requirements of the shift. """ compatibilities = [] + # Iterate through each shift in the request - for asset_request_vehicle in self._asset_request_vehicles_: + for shift in self._shifts_: # Each shift gets its own row in the result set. shift_compatibility = [] - # Shift blocks are the _time_granularity_ sections the volunteer would need to be available for. - # Its calculated by finding all the 30 minute slots between the start time and end time (inclusive) - shift_blocks = self.calculate_deltas(asset_request_vehicle.from_date_time, - asset_request_vehicle.to_date_time) - # Iterate through the users, this makes each element of the array + # Define the shift start and end times + shift_start = shift.startTime + shift_end = shift.endTime + + # Iterate through the users for user in self._users_: - # We start by assuming the user is available, then prove this wrong. + # Start by assuming the user is available user_available = True - # Iterate through each block to see if the user is available on this shift - shift_block_availability = [] - for shift_block in shift_blocks: - available_in_shift = False - for day_availability in user.availabilities[self._week_map_[shift_block.weekday()]]: - # Generate a new datetime object that is the start time and end time of their availability, but - # using the date of the shift block. This lets us calculate availability regardless of the day - # of the year - start_time = self.float_time_to_datetime(day_availability[0], shift_block) - end_time = self.float_time_to_datetime(day_availability[1], shift_block) - if end_time >= shift_block >= start_time: - available_in_shift = True - shift_block_availability.append(available_in_shift) - - # If every element in the shift block availability is true, then the user can do this shift. - if False in shift_block_availability: - user_available = False + # Query unavailability times for the current user + unavailability_records = self._session_.query(UnavailabilityTime).filter( + UnavailabilityTime.userId == user.id + ).all() + + # Check if any unavailability overlaps with the entire shift period + for record in unavailability_records: + if record.start < shift_end and record.end > shift_start: + user_available = False + break # User is unavailable, no need to check further + # Append the user's availability for this shift shift_compatibility.append(user_available) + # Append the shift compatibilities to the overall result compatibilities.append(shift_compatibility) - # Return the 2D array + + # Return the 2D array of compatibilities return compatibilities - def calculate_clashes(self) -> List[List[bool]]: - """ - Generate a 2d array of vehicle requests that overlap. This is to ensure that a single user isn't assigned to - multiple vehicles simultaneously. Its expected that each shift is incompatible with itself too. - @return: A 2D array of clashes. - """ - clashes = [] - # Iterate through each shift in the request - for this_vehicle in self._asset_request_vehicles_: - this_vehicle_clashes = [] - this_shift_blocks = self.calculate_deltas(this_vehicle.from_date_time, - this_vehicle.to_date_time) - for other_vehicle in self._asset_request_vehicles_: - has_clash = False - for this_shift_block in this_shift_blocks: - if other_vehicle.from_date_time <= this_shift_block <= other_vehicle.to_date_time \ - and other_vehicle.id != this_vehicle.id: - has_clash = True - this_vehicle_clashes.append(has_clash) - clashes.append(this_vehicle_clashes) - return clashes + # def calculate_clashes(self) -> List[List[bool]]: + # """ + # Generate a 2d array of vehicle requests that overlap. This is to ensure that a single user isn't assigned to + # multiple vehicles simultaneously. Its expected that each shift is incompatible with itself too. + # @return: A 2D array of clashes. + # """ + # clashes = [] + # # Iterate through each shift in the request + # for this_vehicle in self._asset_request_vehicles_: + # this_vehicle_clashes = [] + # this_shift_blocks = self.calculate_deltas(this_vehicle.from_date_time, + # this_vehicle.to_date_time) + # for other_vehicle in self._asset_request_vehicles_: + # has_clash = False + # for this_shift_block in this_shift_blocks: + # if other_vehicle.from_date_time <= this_shift_block <= other_vehicle.to_date_time \ + # and other_vehicle.id != this_vehicle.id: + # has_clash = True + # this_vehicle_clashes.append(has_clash) + # clashes.append(this_vehicle_clashes) + # return clashes def calculate_skill_requirement(self): """ - Return a 2D array showing the number of people required for each skill in a asset shift. Might look something - like: - Driver Pilot Ninja - ---------------------- - Vehicle 1 [[1, 0, 0] - Vehicle 2 [F, 1, 1]] - @return: + Returns a 2D array showing the number of people required for each skill in an asset shift. Example: + Driver Pilot Ninja + ---------------------- + Shift 1 [[1, 0, 0] + Shift 2 [0, 1, 1]] + Shift 3 [0, 2, 2]] + @return: List of lists containing the required number of people for each role in each shift. """ rtn = [] - for vehicle in self._asset_request_vehicles_: - this_vehicle = [] + + # Iterate through each shift + for shift in self._shifts_: + this_position = [] + + # Iterate through each role for role in self._roles_: - this_vehicle.append(self.get_role_count(vehicle.asset_type.id, role.id)) - rtn.append(this_vehicle) + # Query the number of people required for this role in the current shift + role_count = self._session_.query(func.count(Role.id)) \ + .join(ShiftRequest, ShiftRequest.role_id == Role.id) \ + .filter(ShiftRequest.shift_id == shift.id) \ + .filter(Role.id == role.id) \ + .scalar() # Use scalar() to get the count as an integer + + this_position.append(role_count) + + # Append the role counts for this shift to the return list + rtn.append(this_position) + return rtn def get_role_count(self, asset_type_id, role_id): From fa05bfe47f9b5f3a5a863a16428464c471c9c627 Mon Sep 17 00:00:00 2001 From: "Steven (Quoc)" Date: Tue, 24 Sep 2024 17:01:22 +1000 Subject: [PATCH 3/9] commented out deprecated code --- services/optimiser/calculator.py | 52 +++++++++----------------------- 1 file changed, 14 insertions(+), 38 deletions(-) diff --git a/services/optimiser/calculator.py b/services/optimiser/calculator.py index ac08d25a..3730a83c 100644 --- a/services/optimiser/calculator.py +++ b/services/optimiser/calculator.py @@ -116,20 +116,20 @@ def __get_request_data(self): # .filter(Role.deleted == False) \ # .all() - def calculate_deltas(self, start: datetime, end: datetime) -> List[datetime]: - """ - Given the start time and end time of a shift, generate a list of shift "blocks" which represent a - self._time_granularity_ period that the user would need to be available for - @param start: The start time of the shift. - @param end: The end time of the shift. - @return: A list of dates between the two dates. - """ - deltas = [] - curr = start - while curr < end: - deltas.append(curr) - curr += self._time_granularity_ - return deltas + # def calculate_deltas(self, start: datetime, end: datetime) -> List[datetime]: + # """ + # Given the start time and end time of a shift, generate a list of shift "blocks" which represent a + # self._time_granularity_ period that the user would need to be available for + # @param start: The start time of the shift. + # @param end: The end time of the shift. + # @return: A list of dates between the two dates. + # """ + # deltas = [] + # curr = start + # while curr < end: + # deltas.append(curr) + # curr += self._time_granularity_ + # return deltas # @staticmethod # def float_time_to_datetime(float_hours: float, d: datetime) -> datetime: @@ -146,30 +146,6 @@ def calculate_deltas(self, start: datetime, end: datetime) -> List[datetime]: # # Calculate the actual datetime # hours = int(float_hours) # minutes = int((float_hours * 60) % 60) - # return datetime(d.year, d.month, d.day, hours, minutes, 0) - - def calculate_compatibility(self) -> List[bool]: - compatibilities = [] - # Shift blocks are the _time_granularity_ sections the volunteer would need to be available for. - # It's calculated by finding all the 30 minute slots between the start time and end time (inclusive) - shift_block = self.calculate_deltas(self._shift_.startTime, self._shift_.endTime) - for user in self._users_: - available = True # assume volunteer is available - # first pull then calculate the unavailability block of the user and check if it overlaps with the shift block - unavailabilities = self._session_.query(UnavailabilityTime) \ - .filter(UnavailabilityTime.userId == user.id)\ - .all() - for unavailability in unavailabilities: - - - - - - - - """ - original method/s has structure of double, made redundant as times from unavailability_time table is already in datetime format. - """ def calculate_compatibility(self) -> List[List[bool]]: """ From b099dacd3ddb4248ada633a35195f8559e540713 Mon Sep 17 00:00:00 2001 From: "Steven (Quoc)" Date: Thu, 26 Sep 2024 19:06:19 +1000 Subject: [PATCH 4/9] =?UTF-8?q?make=20requirement=20changes=20to=20this=20?= =?UTF-8?q?spec:=201.=09One-to-Many=20Relationship=20between=20ShiftReques?= =?UTF-8?q?t=20and=20ShiftPosition:=20=09=E2=80=A2=09A=20ShiftRequest=20ca?= =?UTF-8?q?n=20have=20multiple=20ShiftPositions.=20=09=E2=80=A2=09A=20Shif?= =?UTF-8?q?tPosition=20belongs=20to=20a=20single=20ShiftRequest.=202.=09Ma?= =?UTF-8?q?ny-to-One=20Relationship=20between=20ShiftPosition=20and=20Role?= =?UTF-8?q?:=20=09=E2=80=A2=09A=20ShiftPosition=20can=20have=20only=20one?= =?UTF-8?q?=20Role.=20=09=E2=80=A2=09Each=20role=20can=20be=20assigned=20t?= =?UTF-8?q?o=20multiple=20ShiftPositions.=203.=09One-to-One=20Relationship?= =?UTF-8?q?=20between=20ShiftPosition=20and=20ShiftRequestVolunteer:=20=09?= =?UTF-8?q?=E2=80=A2=09Each=20ShiftPosition=20is=20assigned=20to=20at=20mo?= =?UTF-8?q?st=20one=20ShiftRequestVolunteer,=20and=20each=20volunteer=20is?= =?UTF-8?q?=20assigned=20to=20exactly=20one=20ShiftPosition.=20this=20is?= =?UTF-8?q?=20to=20ensure=20that=20the=20database=20works=20as=20expected.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit will need to to be reworked in calculator -> get_skill_requirement and get_role_count. --- domain/entity/role.py | 3 +-- domain/entity/shift_position.py | 20 ++++++++++++++++++++ domain/entity/shift_request.py | 8 ++++++-- repository/shift_requirements.py | 21 --------------------- services/optimiser/calculator.py | 5 +---- 5 files changed, 28 insertions(+), 29 deletions(-) create mode 100644 domain/entity/shift_position.py delete mode 100644 repository/shift_requirements.py diff --git a/domain/entity/role.py b/domain/entity/role.py index 3c1c5fc5..146765ff 100644 --- a/domain/entity/role.py +++ b/domain/entity/role.py @@ -6,8 +6,7 @@ class Role(Base): __tablename__ = 'role' id = Column(Integer, primary_key=True, autoincrement=True) - shift_request_id = Column(Integer, ForeignKey('shift_request.id'), name='shift_request_id', nullable=False) - code = Column(String(256), nullable=False) + code = Column(String(256), nullable=False, unique=True) # Must be unique name = Column(String(256), nullable=False) deleted = Column(Boolean, nullable=False, default=False) update_date_time = Column(DateTime, name='last_update_datetime', default=datetime.now(), nullable=False) diff --git a/domain/entity/shift_position.py b/domain/entity/shift_position.py new file mode 100644 index 00000000..c7d2a596 --- /dev/null +++ b/domain/entity/shift_position.py @@ -0,0 +1,20 @@ +from datetime import datetime + +from sqlalchemy import Column, String, DateTime, ForeignKey, Integer, Enum +from sqlalchemy.orm import relationship + +from domain.base import Base + + +class ShiftPosition(Base): + __tablename__ = 'position' + + id = Column(Integer, primary_key=True, autoincrement=True) + shift_id = Column(Integer, ForeignKey('shift.id'), nullable=False) + role_code = Column(String(256), ForeignKey('role.code'), nullable=False) + + # Many-to-one relationship with Role + role = relationship("Role") + + # One-to-one relationship with ShiftRequestVolunteer using backref + volunteer = relationship("ShiftRequestVolunteer", uselist=False, backref="position") \ No newline at end of file diff --git a/domain/entity/shift_request.py b/domain/entity/shift_request.py index ca5aebce..cbfd37e1 100644 --- a/domain/entity/shift_request.py +++ b/domain/entity/shift_request.py @@ -7,7 +7,6 @@ from domain.base import Base - class ShiftRequest(Base): __tablename__ = 'shift_request' @@ -20,4 +19,9 @@ class ShiftRequest(Base): update_date_time = Column(DateTime, name='last_update_datetime', default=datetime.now(), nullable=False) insert_date_time = Column(DateTime, name='created_datetime', default=datetime.now(), nullable=False) Column() - user = relationship("User") \ No newline at end of file + user = relationship("User") + + # One-to-many relationship: A shift can have multiple positions + positions = relationship("ShiftPosition", backref="shift_request") + + diff --git a/repository/shift_requirements.py b/repository/shift_requirements.py deleted file mode 100644 index 3a51f779..00000000 --- a/repository/shift_requirements.py +++ /dev/null @@ -1,21 +0,0 @@ -from datetime import datetime - -from sqlalchemy import Column, String, DateTime, ForeignKey, Integer, Enum -from sqlalchemy.orm import relationship - -from domain import ShiftStatus -from domain.base import Base - -class ShiftRequirements(Base): - __tablename__ = 'shift_requirements' - - id = Column(Integer, primary_key=True, autoincrement=True) - request_id = Column(Integer, ForeignKey('shift_request.id'), name='request_id', nullable=False) - role_id = Column(Integer, ForeignKey('role.id'), name='role.id') - - # title = Column(String(29), name='title', nullable=False) - # startTime = Column(DateTime, name='from', nullable=False) - # endTime = Column(DateTime, name='to', nullable=False) - # status = Column(Enum(ShiftStatus), name='status', default=ShiftStatus.WAITING, nullable=False) - # update_date_time = Column(DateTime, name='last_update_datetime', default=datetime.now(), nullable=False) - # insert_date_time = Column(DateTime, name='created_datetime', default=datetime.now(), nullable=False) \ No newline at end of file diff --git a/services/optimiser/calculator.py b/services/optimiser/calculator.py index 3730a83c..446d2cfe 100644 --- a/services/optimiser/calculator.py +++ b/services/optimiser/calculator.py @@ -243,10 +243,7 @@ def calculate_skill_requirement(self): def get_role_count(self, asset_type_id, role_id): """ - Determines the number of each role required for each asset type or 0 if not required. - @param asset_type_id: The asset type to search for. - @param role_id: The role to search for. - @return: The volunteers required or 0 if not required. + check for vehicle type flag and generate a """ query = self._session_.query(AssetTypeRole) \ .join(Role, Role.id == AssetTypeRole.role_id) \ From 71aef6dc7649e1e063ae70b1a3068d11ec4429e1 Mon Sep 17 00:00:00 2001 From: "Steven (Quoc)" Date: Sat, 28 Sep 2024 03:11:48 +1000 Subject: [PATCH 5/9] fixed role counts pray it works --- domain/entity/__init__.py | 3 ++- services/optimiser/calculator.py | 44 +++++++++++++------------------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/domain/entity/__init__.py b/domain/entity/__init__.py index 794360e8..bca21b8b 100644 --- a/domain/entity/__init__.py +++ b/domain/entity/__init__.py @@ -13,4 +13,5 @@ from .unavailability_time import UnavailabilityTime from .chatbot_input import ChatbotInput from .shift_request import ShiftRequest -from .shift_request_volunteer import ShiftRequestVolunteer \ No newline at end of file +from .shift_request_volunteer import ShiftRequestVolunteer +from .shift_position import ShiftPosition diff --git a/services/optimiser/calculator.py b/services/optimiser/calculator.py index 446d2cfe..4b723d3a 100644 --- a/services/optimiser/calculator.py +++ b/services/optimiser/calculator.py @@ -3,7 +3,8 @@ from typing import List from sqlalchemy import orm, func, alias -from domain import User, AssetRequestVehicle, AssetType, Role, UserRole, AssetTypeRole, ShiftRequest, ShiftRequestVolunteer, UnavailabilityTime +from domain import (User, AssetType, Role, UserRole, AssetTypeRole, ShiftRequest, ShiftPosition, + UnavailabilityTime) class Calculator: @@ -16,7 +17,7 @@ class Calculator: # This matters as the lists passed to Minizinc are not keyed and are instead used by index. _users_ = [] _shifts_ = [] - + _positions_ = [] _roles_ = [] @@ -98,14 +99,8 @@ def __get_request_data(self): .all() self._shift_ = self._session_.query(ShiftRequest) \ .all() - - # self._asset_request_vehicles_ = self._session_.query(AssetRequestVehicle) \ - # .filter(AssetRequestVehicle.request_id == self.request_id) \ - # .all() - # self._asset_types_ = self._session_.query(AssetType) \ - # .filter(AssetType.deleted == False) \ - # .all() - + self._positions_ = self._session_.query(ShiftPosition) \ + .all() # return the roles that have not been deleted for the all shifts self._roles_ = self._session_.query(Role) \ .filter(Role.deleted == False) \ @@ -220,37 +215,34 @@ def calculate_skill_requirement(self): @return: List of lists containing the required number of people for each role in each shift. """ rtn = [] - # Iterate through each shift for shift in self._shifts_: this_position = [] - # Iterate through each role for role in self._roles_: - # Query the number of people required for this role in the current shift - role_count = self._session_.query(func.count(Role.id)) \ - .join(ShiftRequest, ShiftRequest.role_id == Role.id) \ - .filter(ShiftRequest.shift_id == shift.id) \ - .filter(Role.id == role.id) \ - .scalar() # Use scalar() to get the count as an integer + # Use the get_role_count function to query the number of people required for this role in the current + # shift + role_count = self.get_role_count(shift.id, role.code) + # Append the role count to the current shift's list this_position.append(role_count) - # Append the role counts for this shift to the return list + # Append the list of role counts for this shift to the return list rtn.append(this_position) return rtn - def get_role_count(self, asset_type_id, role_id): + def get_role_count(self, shift_request_id, role_code): """ - check for vehicle type flag and generate a + given a shift id and a role code, return the number of people required for that specific role + this is done by counting the entries of shift positions that match the role """ - query = self._session_.query(AssetTypeRole) \ - .join(Role, Role.id == AssetTypeRole.role_id) \ - .join(AssetType, AssetType.id == AssetTypeRole.asset_type_id) \ + query = self._session_.query(func.count(ShiftPosition.id)) \ + .join(Role, Role.code == ShiftPosition.role_code) \ .filter(Role.deleted == False) \ - .filter(Role.id == role_id) \ - .filter(AssetType.id == asset_type_id) + .filter(Role.code == role_code) \ + .filter(ShiftPosition.shift_id == shift_request_id) \ + result = self._session_.query(func.count('*')).select_from(alias(query)).scalar() if result is None: result = 0 From dbef7d000efe64d9cc7ef04e3ffd87b3725b389b Mon Sep 17 00:00:00 2001 From: "Steven (Quoc)" Date: Sat, 28 Sep 2024 03:42:54 +1000 Subject: [PATCH 6/9] undo git revert --- domain/entity/__init__.py | 3 +-- services/optimiser/calculator.py | 44 +++++++++++++++++++------------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/domain/entity/__init__.py b/domain/entity/__init__.py index bca21b8b..794360e8 100644 --- a/domain/entity/__init__.py +++ b/domain/entity/__init__.py @@ -13,5 +13,4 @@ from .unavailability_time import UnavailabilityTime from .chatbot_input import ChatbotInput from .shift_request import ShiftRequest -from .shift_request_volunteer import ShiftRequestVolunteer -from .shift_position import ShiftPosition +from .shift_request_volunteer import ShiftRequestVolunteer \ No newline at end of file diff --git a/services/optimiser/calculator.py b/services/optimiser/calculator.py index 4b723d3a..446d2cfe 100644 --- a/services/optimiser/calculator.py +++ b/services/optimiser/calculator.py @@ -3,8 +3,7 @@ from typing import List from sqlalchemy import orm, func, alias -from domain import (User, AssetType, Role, UserRole, AssetTypeRole, ShiftRequest, ShiftPosition, - UnavailabilityTime) +from domain import User, AssetRequestVehicle, AssetType, Role, UserRole, AssetTypeRole, ShiftRequest, ShiftRequestVolunteer, UnavailabilityTime class Calculator: @@ -17,7 +16,7 @@ class Calculator: # This matters as the lists passed to Minizinc are not keyed and are instead used by index. _users_ = [] _shifts_ = [] - _positions_ = [] + _roles_ = [] @@ -99,8 +98,14 @@ def __get_request_data(self): .all() self._shift_ = self._session_.query(ShiftRequest) \ .all() - self._positions_ = self._session_.query(ShiftPosition) \ - .all() + + # self._asset_request_vehicles_ = self._session_.query(AssetRequestVehicle) \ + # .filter(AssetRequestVehicle.request_id == self.request_id) \ + # .all() + # self._asset_types_ = self._session_.query(AssetType) \ + # .filter(AssetType.deleted == False) \ + # .all() + # return the roles that have not been deleted for the all shifts self._roles_ = self._session_.query(Role) \ .filter(Role.deleted == False) \ @@ -215,34 +220,37 @@ def calculate_skill_requirement(self): @return: List of lists containing the required number of people for each role in each shift. """ rtn = [] + # Iterate through each shift for shift in self._shifts_: this_position = [] + # Iterate through each role for role in self._roles_: - # Use the get_role_count function to query the number of people required for this role in the current - # shift - role_count = self.get_role_count(shift.id, role.code) + # Query the number of people required for this role in the current shift + role_count = self._session_.query(func.count(Role.id)) \ + .join(ShiftRequest, ShiftRequest.role_id == Role.id) \ + .filter(ShiftRequest.shift_id == shift.id) \ + .filter(Role.id == role.id) \ + .scalar() # Use scalar() to get the count as an integer - # Append the role count to the current shift's list this_position.append(role_count) - # Append the list of role counts for this shift to the return list + # Append the role counts for this shift to the return list rtn.append(this_position) return rtn - def get_role_count(self, shift_request_id, role_code): + def get_role_count(self, asset_type_id, role_id): """ - given a shift id and a role code, return the number of people required for that specific role - this is done by counting the entries of shift positions that match the role + check for vehicle type flag and generate a """ - query = self._session_.query(func.count(ShiftPosition.id)) \ - .join(Role, Role.code == ShiftPosition.role_code) \ + query = self._session_.query(AssetTypeRole) \ + .join(Role, Role.id == AssetTypeRole.role_id) \ + .join(AssetType, AssetType.id == AssetTypeRole.asset_type_id) \ .filter(Role.deleted == False) \ - .filter(Role.code == role_code) \ - .filter(ShiftPosition.shift_id == shift_request_id) \ - + .filter(Role.id == role_id) \ + .filter(AssetType.id == asset_type_id) result = self._session_.query(func.count('*')).select_from(alias(query)).scalar() if result is None: result = 0 From 88cb8ba697ce75066b2dc54c00c00cd9e2757c51 Mon Sep 17 00:00:00 2001 From: "Steven (Quoc)" Date: Sat, 28 Sep 2024 03:46:13 +1000 Subject: [PATCH 7/9] Revert "undo git revert" This reverts commit dbef7d000efe64d9cc7ef04e3ffd87b3725b389b. --- domain/entity/__init__.py | 3 ++- services/optimiser/calculator.py | 44 +++++++++++++------------------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/domain/entity/__init__.py b/domain/entity/__init__.py index 794360e8..bca21b8b 100644 --- a/domain/entity/__init__.py +++ b/domain/entity/__init__.py @@ -13,4 +13,5 @@ from .unavailability_time import UnavailabilityTime from .chatbot_input import ChatbotInput from .shift_request import ShiftRequest -from .shift_request_volunteer import ShiftRequestVolunteer \ No newline at end of file +from .shift_request_volunteer import ShiftRequestVolunteer +from .shift_position import ShiftPosition diff --git a/services/optimiser/calculator.py b/services/optimiser/calculator.py index 446d2cfe..4b723d3a 100644 --- a/services/optimiser/calculator.py +++ b/services/optimiser/calculator.py @@ -3,7 +3,8 @@ from typing import List from sqlalchemy import orm, func, alias -from domain import User, AssetRequestVehicle, AssetType, Role, UserRole, AssetTypeRole, ShiftRequest, ShiftRequestVolunteer, UnavailabilityTime +from domain import (User, AssetType, Role, UserRole, AssetTypeRole, ShiftRequest, ShiftPosition, + UnavailabilityTime) class Calculator: @@ -16,7 +17,7 @@ class Calculator: # This matters as the lists passed to Minizinc are not keyed and are instead used by index. _users_ = [] _shifts_ = [] - + _positions_ = [] _roles_ = [] @@ -98,14 +99,8 @@ def __get_request_data(self): .all() self._shift_ = self._session_.query(ShiftRequest) \ .all() - - # self._asset_request_vehicles_ = self._session_.query(AssetRequestVehicle) \ - # .filter(AssetRequestVehicle.request_id == self.request_id) \ - # .all() - # self._asset_types_ = self._session_.query(AssetType) \ - # .filter(AssetType.deleted == False) \ - # .all() - + self._positions_ = self._session_.query(ShiftPosition) \ + .all() # return the roles that have not been deleted for the all shifts self._roles_ = self._session_.query(Role) \ .filter(Role.deleted == False) \ @@ -220,37 +215,34 @@ def calculate_skill_requirement(self): @return: List of lists containing the required number of people for each role in each shift. """ rtn = [] - # Iterate through each shift for shift in self._shifts_: this_position = [] - # Iterate through each role for role in self._roles_: - # Query the number of people required for this role in the current shift - role_count = self._session_.query(func.count(Role.id)) \ - .join(ShiftRequest, ShiftRequest.role_id == Role.id) \ - .filter(ShiftRequest.shift_id == shift.id) \ - .filter(Role.id == role.id) \ - .scalar() # Use scalar() to get the count as an integer + # Use the get_role_count function to query the number of people required for this role in the current + # shift + role_count = self.get_role_count(shift.id, role.code) + # Append the role count to the current shift's list this_position.append(role_count) - # Append the role counts for this shift to the return list + # Append the list of role counts for this shift to the return list rtn.append(this_position) return rtn - def get_role_count(self, asset_type_id, role_id): + def get_role_count(self, shift_request_id, role_code): """ - check for vehicle type flag and generate a + given a shift id and a role code, return the number of people required for that specific role + this is done by counting the entries of shift positions that match the role """ - query = self._session_.query(AssetTypeRole) \ - .join(Role, Role.id == AssetTypeRole.role_id) \ - .join(AssetType, AssetType.id == AssetTypeRole.asset_type_id) \ + query = self._session_.query(func.count(ShiftPosition.id)) \ + .join(Role, Role.code == ShiftPosition.role_code) \ .filter(Role.deleted == False) \ - .filter(Role.id == role_id) \ - .filter(AssetType.id == asset_type_id) + .filter(Role.code == role_code) \ + .filter(ShiftPosition.shift_id == shift_request_id) \ + result = self._session_.query(func.count('*')).select_from(alias(query)).scalar() if result is None: result = 0 From 49d1a67ce889ed5b8e3d3e56bc0cba6688c79287 Mon Sep 17 00:00:00 2001 From: "Steven (Quoc)" Date: Mon, 30 Sep 2024 17:48:09 +1000 Subject: [PATCH 8/9] - removed depracated code comments - fixed shift_request.id reference in ShiftPosition - modifed ShiftPostion table name from position -> shift_position --- domain/entity/shift_position.py | 6 +-- services/optimiser/calculator.py | 89 -------------------------------- 2 files changed, 3 insertions(+), 92 deletions(-) diff --git a/domain/entity/shift_position.py b/domain/entity/shift_position.py index c7d2a596..435c850d 100644 --- a/domain/entity/shift_position.py +++ b/domain/entity/shift_position.py @@ -7,14 +7,14 @@ class ShiftPosition(Base): - __tablename__ = 'position' + __tablename__ = 'shift_position' id = Column(Integer, primary_key=True, autoincrement=True) - shift_id = Column(Integer, ForeignKey('shift.id'), nullable=False) + shift_id = Column(Integer, ForeignKey('shift_request.id'), nullable=False) role_code = Column(String(256), ForeignKey('role.code'), nullable=False) # Many-to-one relationship with Role role = relationship("Role") # One-to-one relationship with ShiftRequestVolunteer using backref - volunteer = relationship("ShiftRequestVolunteer", uselist=False, backref="position") \ No newline at end of file + volunteer = relationship("ShiftRequestVolunteer", uselist=False, backref="shift_position") \ No newline at end of file diff --git a/services/optimiser/calculator.py b/services/optimiser/calculator.py index 4b723d3a..c4c158e7 100644 --- a/services/optimiser/calculator.py +++ b/services/optimiser/calculator.py @@ -25,24 +25,9 @@ class Calculator: # function. _session_ = None - # This is the granularity of the optimiser, it won't consider any times more specific thann this number of minutes - # when scheduling employees. - # It should match the volunteer's shift planner granularity - _time_granularity_ = timedelta(minutes=30) - # The request to optimise. request_id = None - # Used to map between datetime.datetime().weekday() to the users availability as its agnostic of the time of year. - _week_map_ = { - 0: "Monday", - 1: "Tuesday", - 2: "Wednesday", - 3: "Thursday", - 4: "Friday", - 5: "Saturday", - 6: "Sunday" - } def __init__(self, session: orm.session, request_id: int): self._session_ = session @@ -51,16 +36,6 @@ def __init__(self, session: orm.session, request_id: int): # Fetch all the request data that will be used in the optimisation functions once. self.__get_request_data() - def get_number_of_vehicles(self) -> int: - """ - @return: The number of vehicles to be optimised. - """ - return len(self._asset_request_vehicles_) - ## vehicle_count = self._asset_request_vehicles_.filter( asset_request_vehicles.vheicle_id == self.request_id, - ## asset_request_vehicles.vehicle_id == self.vehicle_id # Compare the vehicle ID - ## ).count() - - return vehicle_count def get_number_of_roles(self): """ @@ -80,12 +55,6 @@ def get_volunteer_by_index(self, index) -> User: def get_role_by_index(self, index) -> Role: return self._roles_[index] - # def get_asset_request_by_index(self, index) -> AssetRequestVehicle: - # return self._asset_request_vehicles_[index] - # - # def get_asset_requests(self) -> List[AssetRequestVehicle]: - # return self._asset_request_vehicles_ - def get_roles(self) -> List[Role]: return self._roles_ @@ -106,42 +75,6 @@ def __get_request_data(self): .filter(Role.deleted == False) \ .all() - # self._asset_type_seats_ = self._session_.query(AssetTypeRole) \ - # .join(Role, Role.id == AssetTypeRole.role_id) \ - # .filter(Role.deleted == False) \ - # .all() - - # def calculate_deltas(self, start: datetime, end: datetime) -> List[datetime]: - # """ - # Given the start time and end time of a shift, generate a list of shift "blocks" which represent a - # self._time_granularity_ period that the user would need to be available for - # @param start: The start time of the shift. - # @param end: The end time of the shift. - # @return: A list of dates between the two dates. - # """ - # deltas = [] - # curr = start - # while curr < end: - # deltas.append(curr) - # curr += self._time_granularity_ - # return deltas - - # @staticmethod - # def float_time_to_datetime(float_hours: float, d: datetime) -> datetime: - # """ - # Given a users available time as a date agnostic decimal hour and a shift blocks date, combine the two into a - # datetime that can be used for equality and range comparisons. - # @param float_hours: The users decimal hour availability, i.e. 3.5 is 3:30am, 4.0 is 4am, - # @param d: The shift blocks date time - # @return: The decimal hours time on the shift blocks day as datetime - # """ - # # Assertion to ensure the front end garbage hasn't continued - # assert 0 <= float_hours <= 23.5 - # - # # Calculate the actual datetime - # hours = int(float_hours) - # minutes = int((float_hours * 60) % 60) - def calculate_compatibility(self) -> List[List[bool]]: """ Generates a 2D array of compatibilities between volunteers' unavailability and the requirements of the shift. @@ -182,28 +115,6 @@ def calculate_compatibility(self) -> List[List[bool]]: # Return the 2D array of compatibilities return compatibilities - # def calculate_clashes(self) -> List[List[bool]]: - # """ - # Generate a 2d array of vehicle requests that overlap. This is to ensure that a single user isn't assigned to - # multiple vehicles simultaneously. Its expected that each shift is incompatible with itself too. - # @return: A 2D array of clashes. - # """ - # clashes = [] - # # Iterate through each shift in the request - # for this_vehicle in self._asset_request_vehicles_: - # this_vehicle_clashes = [] - # this_shift_blocks = self.calculate_deltas(this_vehicle.from_date_time, - # this_vehicle.to_date_time) - # for other_vehicle in self._asset_request_vehicles_: - # has_clash = False - # for this_shift_block in this_shift_blocks: - # if other_vehicle.from_date_time <= this_shift_block <= other_vehicle.to_date_time \ - # and other_vehicle.id != this_vehicle.id: - # has_clash = True - # this_vehicle_clashes.append(has_clash) - # clashes.append(this_vehicle_clashes) - # return clashes - def calculate_skill_requirement(self): """ Returns a 2D array showing the number of people required for each skill in an asset shift. Example: From 75bc479bb96a8dd56d9fcc1269ddcfc8492a0da4 Mon Sep 17 00:00:00 2001 From: "Steven (Quoc)" Date: Mon, 30 Sep 2024 18:47:25 +1000 Subject: [PATCH 9/9] explicit shift request volunteer declaration in ShiftPosition --- domain/entity/shift_position.py | 3 ++- domain/entity/shift_request_volunteer.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/domain/entity/shift_position.py b/domain/entity/shift_position.py index 435c850d..ee41a167 100644 --- a/domain/entity/shift_position.py +++ b/domain/entity/shift_position.py @@ -17,4 +17,5 @@ class ShiftPosition(Base): role = relationship("Role") # One-to-one relationship with ShiftRequestVolunteer using backref - volunteer = relationship("ShiftRequestVolunteer", uselist=False, backref="shift_position") \ No newline at end of file + volunteer = relationship("ShiftRequestVolunteer", uselist=False, backref="shift_position", + primaryjoin="ShiftPosition.id == ShiftRequestVolunteer.position_id") diff --git a/domain/entity/shift_request_volunteer.py b/domain/entity/shift_request_volunteer.py index b73447f0..93f2de9b 100644 --- a/domain/entity/shift_request_volunteer.py +++ b/domain/entity/shift_request_volunteer.py @@ -13,7 +13,7 @@ class ShiftRequestVolunteer(Base): id = Column(Integer, primary_key=True, autoincrement=True) user_id = Column(Integer, ForeignKey('user.id'), name='user_id', nullable=False) request_id = Column(Integer, ForeignKey('shift_request.id'), name='request_id', nullable=False) - position_id = Column(Integer, ForeignKey('role.id'), name="role_id", nullable=False) + position_id = Column(Integer, ForeignKey('shift_position.id'), name="position_id", nullable=False) status = Column(Enum(ShiftVolunteerStatus), name='status', nullable=False, default=ShiftVolunteerStatus.PENDING) update_date_time = Column(DateTime, name='last_update_datetime', default=datetime.now(), nullable=False) insert_date_time = Column(DateTime, name='created_datetime', default=datetime.now(), nullable=False)