Skip to content

Commit

Permalink
Yiran li/feature/create shift api v2 (#301)
Browse files Browse the repository at this point in the history
## Describe your changes
In this PR, we mainly implemented the functionality for creating new
shift requests. I also enhanced the `update_status` function by
introducing a conflict check when the status is changed to accepted,
ensuring that any conflicting shifts are detected before the status is
updated. Additionally, an unavailability time record is created when a
shift is accepted. Furthermore, we made modifications to the database
schema by adding the `shift_position` table and establishing
relationships between the `shift_request`, `shift_request_volunteer`,
and `shift_position` tables.
### Functional changes
#### Create new shift
We added functionality to allow users to create new shift requests.
These requests include details such as title, start time, end time, and
vehicle type. Based on the vehicle type, the system determines the
number and types of volunteer roles (such as 'Crew Leader', 'Driver',
'Advanced', and 'Basic') required for the shift. These roles are then
stored in the `shift_position` table, ensuring that each shift has the
correct set of roles assigned to it.
#### Update Status
The `update_status` function was enhanced to incorporate conflict
detection when a shift status is changed to accepted. When the status is
updated to accepted, the system performs a conflict check by querying
the database for any other confirmed shifts assigned to the same
volunteer that overlap in time. If a conflict is detected, the status
change is rejected, and a `ConflictError` is raised. If no conflict is
found, the status is updated, and an `unavailability_time` record is
created, marking the volunteer's time as unavailable during the shift
period. This ensures accurate scheduling and prevents volunteers from
being double-booked.
Additionally, the function handles updating shifts to other statuses,
such as rejected. When a shift is rejected, the status is simply updated
without further checks.
### Database Changes
### Add `shift_position` table
The `shift_position` table was introduced to store information about the
roles required for each shift based on the type of vehicle involved. The
`create_positions` function dynamically generates positions for each
shift according to the vehicle_type. Depending on the vehicle type, a
predefined set of roles (e.g., crewLeader, driver, advanced, basic) is
assigned to the shift. For example:
**Heavy Tanker**: Six roles are assigned—crewLeader, driver, two
advanced, and two basic roles.
**Medium Tanker**: Four roles are assigned—crewLeader, driver, advanced,
and basic.
**Light Unit**: Two roles are assigned—driver and basic.
Each role is saved as a record in the `shift_position` table, with the
associated `shift_id` and `role_code`. This flexible design allows
different vehicle types to require varying numbers and types of
volunteers for each shift.
#### Modify `unavailability_time` table
The `unavailability_time` table was updated with a new column called
`is_shift`. This column is a boolean field that indicates whether the
unavailability time corresponds to a scheduled shift. When a volunteer
accepts a shift, a corresponding unavailability time record is created
with the `is_shift` flag set to True. This allows the system to
distinguish between general unavailability times and those specifically
associated with accepted shifts.
#### Status Enum Updates
The status enums were updated for both `ShiftStatus` and
`ShiftVolunteerStatus` to reflect clearer stages in the shift lifecycle.
For ShiftRequest, statuses such as "pending," "submitted," "confirmed,"
and "completed" were introduced. For ShiftRequestVolunteer, "accepted"
replaced the former "confirmed" status to better align with the system's
workflow.
###
I have already done some basic test for these updates in Postman.
## Issue ticket number and link

https://fireapp-emergiq-2024.atlassian.net/browse/FIR-112?atlOrigin=eyJpIjoiNmMyYmJiODlhZWI5NGUwY2JjYmIwNzkwMGY5N2M3ZTMiLCJwIjoiaiJ9

---------

Co-authored-by: anannnchim <[email protected]>
Co-authored-by: Steven (Quoc) <[email protected]>
  • Loading branch information
3 people authored Oct 5, 2024
1 parent 58313c3 commit 91fbcb9
Show file tree
Hide file tree
Showing 15 changed files with 233 additions and 57 deletions.
42 changes: 33 additions & 9 deletions controllers/v2/shift/api.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from flask_restful import reqparse, Resource, marshal_with, inputs, marshal

from exception.client_exception import ConflictError
from .response_models import shift
from domain import UserType
from domain import UserType, ShiftVolunteerStatus
from repository.shift_repository import ShiftRepository
from services.jwk import requires_auth, is_user_or_has_role
from services.jwk import requires_auth, is_user_or_has_role, requires_admin, has_role
from controllers.v2.v2_blueprint import v2_api
import logging

Expand All @@ -11,7 +13,8 @@
parser.add_argument('title', type=str)
parser.add_argument('start', type=inputs.datetime_from_iso8601, required=True, help="Start time cannot be blank!")
parser.add_argument('end', type=inputs.datetime_from_iso8601, required=True, help="End time cannot be blank!")
parser.add_argument('roles', type=list, location='json', required=True, help="Roles cannot be blank!")
parser.add_argument('vehicle_type', type=int, required=True, help="Vehicle type cannot be blank!")

parser_modify_status = reqparse.RequestParser()
parser_modify_status.add_argument('status', type=str, location='json', required=True, help="Status cannot be blank!")

Expand All @@ -22,29 +25,50 @@ class VolunteerShiftV2(Resource):
def __init__(self, shift_repository: ShiftRepository = ShiftRepository()):
self.shift_repository = shift_repository

@requires_auth
@has_role(UserType.ROOT_ADMIN)
def post(self, user_id):
try:
args = parser.parse_args()
title = args['title']
start = args['start']
end = args['end']
vehicle_type = args['vehicle_type']
new_shift_id = self.shift_repository.post_shift_request(user_id, title, start, end, vehicle_type)
if new_shift_id:
return {"shift_id": new_shift_id}, 200
else:
return {"message": "Failed to create shift."}, 400
except Exception as e:
logging.error(f"Error creating new shift request: {e}")
return {"message": "Internal server error"}, 500


@requires_auth
@is_user_or_has_role(None, UserType.ROOT_ADMIN)
def get(self, user_id):
try:
shifts = self.shift_repository.get_shift(user_id)
if shifts:
return marshal(shifts, shift), 200
else:
return {"message": "No shift record found."}, 400
return marshal(shifts, shift), 200
except Exception as e:
logging.error(f"Error retrieving shifts for user {user_id}: {e}")
return {"message": "Internal server error"}, 500


@requires_auth
@is_user_or_has_role(None, UserType.ROOT_ADMIN)
def put(self, user_id, shift_id):
args = parser_modify_status.parse_args()
status = args["status"]
status_enum = ShiftVolunteerStatus[status.upper()]
try:
success = self.shift_repository.update_shift_status(user_id, shift_id, status)
success = self.shift_repository.update_shift_status(user_id, shift_id, status_enum)
if success:
return {"message": "Status updated successfully"}, 200
else:
return {"message": "No user or shift record is found, status not updated."}, 400
except ConflictError as e: # Handle conflict error
logging.error(f"Conflict when updating shift for user {user_id}: {e}")
return {"message": "Shift time conflict detected. Cannot confirm shift."}, 409
except Exception as e:
logging.error(f"Error updating shifts for user {user_id}: {e}")
return {"message": "Internal server error"}, 500
Expand Down
4 changes: 2 additions & 2 deletions controllers/v2/shift/response_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

shift = {
'shiftId': fields.Integer,
'status': fields.String,
'title': fields.String,
'start': fields.DateTime(dt_format='iso8601'),
'end': fields.DateTime(dt_format='iso8601')
'end': fields.DateTime(dt_format='iso8601'),
'status': fields.String
}
11 changes: 5 additions & 6 deletions controllers/v2/unavailability/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,11 @@ def __init__(self, event_repository: EventRepository = EventRepository()):
@requires_auth
@is_user_or_has_role(None, UserType.ROOT_ADMIN)
def get(self, user_id):
volunteer_unavailability_record = self.event_repository.get_event(user_id)
if volunteer_unavailability_record is not None and volunteer_unavailability_record != []:
return volunteer_unavailability_record
elif volunteer_unavailability_record == []:
return {"message": "No unavailability record found."}, 400
else:
try:
volunteer_unavailability_record = self.event_repository.get_event(user_id)
return volunteer_unavailability_record, 200
except Exception as e:
logging.error(f"Error retrieving shifts for user {user_id}: {e}")
return {"message": "Internal server error"}, 500

@requires_auth
Expand Down
3 changes: 1 addition & 2 deletions domain/entity/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,4 @@
from .shift_request import ShiftRequest
from .shift_request_volunteer import ShiftRequestVolunteer
from .shift_position import ShiftPosition
from .fcm_tokens import FCMToken

from .fcm_tokens import FCMToken
11 changes: 3 additions & 8 deletions domain/entity/shift_position.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
from datetime import datetime

from sqlalchemy import Column, String, DateTime, ForeignKey, Integer, Enum
from sqlalchemy import Column, String, ForeignKey, Integer
from sqlalchemy.orm import relationship

from domain.base import Base


class ShiftPosition(Base):
__tablename__ = 'shift_position'

id = Column(Integer, primary_key=True, autoincrement=True)
shift_id = Column(Integer, ForeignKey('shift_request.id'), nullable=False)
role_code = Column(String(256), ForeignKey('role.code'), nullable=False)
shift_id = Column(Integer, ForeignKey('shift_request.id'), name='shift_id', nullable=False)
role_code = Column(String(256), ForeignKey('role.code'), name='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="shift_position",
primaryjoin="ShiftPosition.id == ShiftRequestVolunteer.position_id")
10 changes: 5 additions & 5 deletions domain/entity/shift_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ class ShiftRequest(Base):
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)
status = Column(Enum(ShiftStatus), name='status', default=ShiftStatus.SUBMITTED, 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")


user = relationship("User")
# One-to-many relationship: A shift can have multiple positions
positions = relationship("ShiftPosition", backref="shift_request")


# One-to-many relationship: A shift can be assigned to many volunteers
volunteers = relationship("ShiftRequestVolunteer", backref="shift_request")
1 change: 0 additions & 1 deletion domain/entity/shift_request_volunteer.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,4 @@ class ShiftRequestVolunteer(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)

shift_request = relationship("ShiftRequest")
user = relationship("User")
1 change: 1 addition & 0 deletions domain/entity/unavailability_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ class UnavailabilityTime(Base):
start = Column(DateTime, nullable=False, default=datetime.now())
end = Column(DateTime, nullable=False, default=datetime.now())
status = Column(Boolean, nullable=False, default=1)
is_shift = Column(Boolean, nullable=False, default=False)
UniqueConstraint(eventId, userId, name='event')
4 changes: 2 additions & 2 deletions domain/type/shift_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
@dataclass
class ShiftRecord:
shiftId: int
status: ShiftVolunteerStatus
title: str
start: datetime
end: datetime
end: datetime
status: ShiftVolunteerStatus
6 changes: 3 additions & 3 deletions domain/type/shift_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@


class ShiftStatus(Enum):
WAITING = "waiting"
UNSUBMITTED = "un-submitted"
INPROGRESS = "in-progress"
PENDING = "pending"
SUBMITTED = "submitted"
CONFIRMED = "confirmed"
COMPLETED = "completed"
2 changes: 1 addition & 1 deletion domain/type/shift_volunteer_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@

class ShiftVolunteerStatus(Enum):
PENDING = "pending"
CONFIRMED = "confirmed"
ACCEPTED = "accepted"
REJECTED = "rejected"
9 changes: 9 additions & 0 deletions exception/client_exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,12 @@ def __init__(self, *args):
def __str__(self):
# Optionally customize the string representation for this specific error
return f"InvalidArgumentError: unexpected values in the payload"

class ConflictError(FireAppException):
def __init__(self, *args):
# Call the superclass constructor with a default message and any additional arguments
super().__init__(f"Shift time conflict detected", *args)

def __str__(self):
# Optionally customize the string representation for this specific error
return f"ConflictError: This shift is conflict with other confirmed shift."
Loading

0 comments on commit 91fbcb9

Please sign in to comment.