Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into mclean/feature/securi…
Browse files Browse the repository at this point in the history
…ty-improvement
  • Loading branch information
emilymclean committed Sep 3, 2024
2 parents 717f529 + d2b6322 commit 5f4e936
Show file tree
Hide file tree
Showing 22 changed files with 692 additions and 260 deletions.
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* @BenMMcLean @naychithanshwe
* @BenMMcLean @ethantr
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
FROM minizinc/minizinc:2.5.5

LABEL org.opencontainers.image.source=https://github.com/TechlauncherFireApp/backend
LABEL org.opencontainers.image.description="A web API designed to efficiently manage and schedule volunteer firefighters."
LABEL org.opencontainers.image.licenses=MIT

# Install Python3.8 and some packages
RUN apt update \
&& DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt install -yqq pkg-config wget git gnupg curl python3.8 python3-pip libmysqlclient-dev
Expand Down
21 changes: 11 additions & 10 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,33 @@ verify_ssl = true
name = "pypi"

[packages]
alembic = "==1.12.0"
alembic = "==1.13.2"
aniso8601 = "==9.0.1"
appdirs = "==1.4.4"
arrow = "==1.3.0"
bcrypt = "==4.1.2"
bcrypt = "==4.2.0"
boto3 = "==1.17.112"
botocore = "==1.20.112"
certifi = "==2024.2.2"
cffi = "==1.16.0"
certifi = "==2024.7.4"
cffi = "==1.17.0"
click = ">=7.1.2"
distlib = "==0.3.8"
filelock = "==3.13.4"
flask-cors = "==4.0.0"
filelock = "==3.14.0"
flask-cors = "==5.0.0"
flask-restful = "==0.3.10"
flask = "==3.0.3"
greenlet = "==3.0.3"
ics = "==0.7.2"
itsdangerous = ">=1.1.0"
jinja2 = ">=2.11.3"
jmespath = "==0.10.0"
mako = "==1.3.3"
mako = "==1.3.5"
markupsafe = ">=1.1.1"
minizinc = "==0.9.0"
mysqlclient = "==2.2.4"
pipenv = "*"
pycparser = "==2.22"
pyjwt = "==2.8.0"
pyjwt = "==2.9.0"
python-dateutil = "==2.9.0.post0"
python-dotenv = "==1.0.1"
python-editor = "==1.0.4"
Expand All @@ -39,15 +39,16 @@ pytz = "==2024.1"
s3transfer = "==0.4.2"
sendgrid = "==6.11.0"
six = "==1.16.0"
sqlalchemy = "==2.0.29"
sqlalchemy = "==2.0.31"
starkbank-ecdsa = "==2.2.0"
tatsu = "==5.8.3"
urllib3 = "==1.26.18"
urllib3 = "==1.26.20"
virtualenv-clone = "==0.5.7"
virtualenv = "==20.25.1"
werkzeug = ">=1.0.1"
numpy = "==1.24.4"
pytest = "*"
dataclasses = "*"

[dev-packages]

Expand Down
455 changes: 243 additions & 212 deletions Pipfile.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion controllers/v2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
from .reference import *
from .diet import *
from .v2_blueprint import v2_api, v2_bp
from .unavailability import *
from .unavailability import *
from .shift import *
2 changes: 2 additions & 0 deletions controllers/v2/shift/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .api import VolunteerShiftV2
from .response_models import shift
52 changes: 52 additions & 0 deletions controllers/v2/shift/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from flask_restful import reqparse, Resource, marshal_with, inputs, marshal
from .response_models import shift
from domain import UserType
from repository.shift_repository import ShiftRepository
from services.jwk import requires_auth, is_user_or_has_role
from controllers.v2.v2_blueprint import v2_api
import logging


parser = reqparse.RequestParser()
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_modify_status = reqparse.RequestParser()
parser_modify_status.add_argument('status', type=str, location='json', required=True, help="Status cannot be blank!")


class VolunteerShiftV2(Resource):
shift_repository: ShiftRepository

def __init__(self, shift_repository: ShiftRepository = ShiftRepository()):
self.shift_repository = shift_repository

@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
except Exception as e:
logging.error(f"Error retrieving shifts for user {user_id}: {e}")
return {"message": "Internal server error"}, 500


def put(self, user_id, shift_id):
args = parser_modify_status.parse_args()
status = args["status"]
try:
success = self.shift_repository.update_shift_status(user_id, shift_id, status)
if success:
return {"message": "Status updated successfully"}, 200
else:
return {"message": "No user or shift record is found, status not updated."}, 400
except Exception as e:
logging.error(f"Error updating shifts for user {user_id}: {e}")
return {"message": "Internal server error"}, 500

v2_api.add_resource(VolunteerShiftV2,'/v2/volunteers/<user_id>/shift','/v2/volunteers/<user_id>/shift/<shift_id>')
9 changes: 9 additions & 0 deletions controllers/v2/shift/response_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from flask_restful import fields

shift = {
'shiftId': fields.Integer,
'status': fields.String,
'title': fields.String,
'start': fields.DateTime(dt_format='iso8601'),
'end': fields.DateTime(dt_format='iso8601')
}
4 changes: 3 additions & 1 deletion domain/entity/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@
from .question import Question
from .password_retrieval import PasswordRetrieval
from .unavailability_time import UnavailabilityTime
from .chatbot_input import ChatbotInput
from .chatbot_input import ChatbotInput
from .shift_request import ShiftRequest
from .shift_request_volunteer import ShiftRequestVolunteer
23 changes: 23 additions & 0 deletions domain/entity/shift_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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 ShiftRequest(Base):
__tablename__ = 'shift_request'

id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey('user.id'), name='user_id', nullable=False)
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)

user = relationship("User")
21 changes: 21 additions & 0 deletions domain/entity/shift_request_volunteer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from datetime import datetime

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

from domain import ShiftVolunteerStatus
from domain.base import Base


class ShiftRequestVolunteer(Base):
__tablename__ = 'shift_request_volunteer'

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)
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)

shift_request = relationship("ShiftRequest")
user = relationship("User")
3 changes: 3 additions & 0 deletions domain/type/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
from .forgot_password_result import ForgotPassword
from .Verify_code import VerifyCode
from .reset_password import ResetPassword
from .shift_record import ShiftRecord
from .shift_status import ShiftStatus
from .shift_volunteer_status import ShiftVolunteerStatus
13 changes: 13 additions & 0 deletions domain/type/shift_record.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from dataclasses import dataclass
from datetime import datetime

from domain.type.shift_volunteer_status import ShiftVolunteerStatus


@dataclass
class ShiftRecord:
shiftId: int
status: ShiftVolunteerStatus
title: str
start: datetime
end: datetime
8 changes: 8 additions & 0 deletions domain/type/shift_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from enum import Enum


class ShiftStatus(Enum):
WAITING = "waiting"
UNSUBMITTED = "un-submitted"
INPROGRESS = "in-progress"
COMPLETED = "completed"
7 changes: 7 additions & 0 deletions domain/type/shift_volunteer_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from enum import Enum


class ShiftVolunteerStatus(Enum):
PENDING = "pending"
CONFIRMED = "confirmed"
REJECTED = "rejected"
4 changes: 2 additions & 2 deletions exception/client_exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def __init__(self, event_id, *args):

def __str__(self):
# Optionally customize the string representation for this specific error
return f"{self.message}: Event ID {self.event_id} could not be located."
return f"Event not found: Event ID {self.event_id} could not be located."


class InvalidArgumentError(FireAppException):
Expand All @@ -17,4 +17,4 @@ def __init__(self, *args):

def __str__(self):
# Optionally customize the string representation for this specific error
return f"{self.message}: unexpected values in the payload"
return f"InvalidArgumentError: unexpected values in the payload"
87 changes: 87 additions & 0 deletions repository/shift_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import logging
from typing import List

from dataclasses import asdict
from flask import jsonify

from datetime import datetime, timezone

from exception import EventNotFoundError, InvalidArgumentError
from domain import session_scope, ShiftRequestVolunteer, ShiftRequest, ShiftRecord


class ShiftRepository:

def __init__(self):
pass

def get_shift(self, userId) -> List[ShiftRecord]:
"""
Retrieves all shift events for a given user that have not ended yet.
:param userId: ID of the user whose shifts are being queried.
:return: A list of shift records or an empty list if none found.
"""
now = datetime.now()
with session_scope() as session:
try:
# only show the shift that is end in the future
shifts = session.query(ShiftRequestVolunteer).join(ShiftRequest).filter(
ShiftRequestVolunteer.user_id == userId,
ShiftRequest.endTime > now).all()
# check if there's some results
if not shifts:
logging.info(f"No active shifts found for user {userId}")
return []
shift_records = []
# write shift information into list
for shift in shifts:
shift_record = ShiftRecord(
shiftId=shift.request_id,
status=shift.status.value,
title=shift.shift_request.title,
start=shift.shift_request.startTime,
end=shift.shift_request.endTime)
shift_records.append(shift_record)
return shift_records
except Exception as e:
logging.error(f"Error retrieving shifts for user {userId}: {e}")
return []

def update_shift_status(self, user_id, shift_id, new_status):
"""
Updates the status of a volunteer's shift request in the database.
Parameters:
----------
user_id : int
The ID of the user whose shift request status is to be updated.
shift_id : int
The ID of the shift request to be updated.
new_status : str
The new status to set for the shift request.
Returns:
-------
bool
Returns `True` if the status was successfully updated, or `False` if the update
failed due to the shift request not being found or an error occurring during the update.
"""
with session_scope() as session:
try:
# Fetch the shift based on user_id
shift_request_volunteer = session.query(ShiftRequestVolunteer).filter_by(
user_id=user_id, request_id=shift_id).first()

# If record exists, update the status
if shift_request_volunteer:
shift_request_volunteer.status = new_status
shift_request_volunteer.last_update_datetime = datetime.now()
session.commit()
return True
else:
logging.info(f"No shift request volunteer with user id {user_id} and shift {shift_id} not found")
return False
except Exception as e:
session.rollback()
logging.error(f"Error updating shift request for user {user_id} and shift_id {shift_id}: {e}")
return False
Loading

0 comments on commit 5f4e936

Please sign in to comment.