-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
LLSC-24: Scheduling API #18
base: main
Are you sure you want to change the base?
Changes from 29 commits
5a0d435
c8907d9
05ac366
cf4cff6
287d434
5f8fb34
b44baf9
a2bc89a
50c554e
a1abe9b
637db5c
7caddb5
5ad9a78
6a72e13
e52dad2
c925fff
f2e74a0
55d8848
789ffc8
37091d0
4b4bd41
4bf5014
a845a46
d353aea
c43780d
8c200f2
c628efb
488a556
c6d711c
e669131
96a2e24
b658eb9
8ccb174
76f8365
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
from abc import ABC, abstractmethod | ||
from app.schemas.schedule import ScheduleCreate, ScheduleInDB, ScheduleAdd, ScheduleData, ScheduleRemove | ||
|
||
class IScheduleService(ABC): | ||
""" | ||
ScheduleService interface | ||
""" | ||
|
||
@abstractmethod | ||
def get_schedule_by_id(self, schedule_id): | ||
""" | ||
Get schedule associated with schedule_id | ||
|
||
:param schedule_id: schedule's id | ||
:type schedule: str | ||
:return: a ScheduleDTO with schedule's information | ||
:rtype: ScheduleDTO | ||
:raises Exception: if schedule retrieval fails | ||
""" | ||
pass | ||
|
||
@abstractmethod | ||
def create_schedule(self, schedule: ScheduleCreate) -> ScheduleInDB: | ||
pass | ||
|
||
|
||
# create schedule | ||
sunbagel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# update schedule status | ||
|
||
# delete schedule | ||
|
||
# add timeblock | ||
# edit timeblock | ||
# remove timeblock | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import uuid | ||
|
||
from sqlalchemy import Column, DateTime, Enum, Interval, Integer, ForeignKey | ||
from sqlalchemy.orm import relationship | ||
from app.models.ScheduleState import ScheduleState | ||
|
||
from .Base import Base | ||
|
||
|
||
class Schedule(Base): | ||
__tablename__ = "schedules" | ||
|
||
id = Column(Integer, primary_key=True) | ||
scheduled_time = Column(DateTime, nullable = True) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's rename this to |
||
duration = Column(Interval, nullable = True) | ||
state_id = Column(Integer, ForeignKey("schedule_states.id"), nullable=False) | ||
|
||
state = relationship("ScheduleState") | ||
time_blocks = relationship("TimeBlock", back_populates="schedule") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from sqlalchemy import Column, Integer, String | ||
|
||
from .Base import Base | ||
|
||
|
||
class ScheduleState(Base): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's rename this to |
||
__tablename__ = "schedule_states" | ||
id = Column(Integer, primary_key=True) | ||
name = Column(String(80), nullable=False) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import uuid | ||
|
||
from sqlalchemy import Column, DateTime, ForeignKey, Integer | ||
from sqlalchemy.dialects.postgresql import UUID | ||
from sqlalchemy.orm import relationship | ||
|
||
from .Base import Base | ||
|
||
|
||
class TimeBlock(Base): | ||
__tablename__ = "time_blocks" | ||
id = Column(Integer, primary_key=True) | ||
schedule_id = Column(Integer, ForeignKey("schedules.id"), nullable = False) | ||
start_time = Column(DateTime) | ||
end_time = Column(DateTime) | ||
|
||
schedule = relationship("Schedule", back_populates="time_blocks") | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
|
||
from fastapi import APIRouter, Depends, HTTPException | ||
from sqlalchemy.orm import Session | ||
|
||
from app.schemas.schedule import ScheduleCreate, ScheduleInDB | ||
from app.services.implementations.schedule_service import ScheduleService | ||
from app.utilities.db_utils import get_db | ||
|
||
|
||
|
||
router = APIRouter( | ||
prefix="/schedules", | ||
tags=["schedules"], | ||
) | ||
|
||
|
||
def get_schedule_service(db: Session = Depends(get_db)): | ||
return ScheduleService(db) | ||
|
||
@router.post("/", response_model=ScheduleInDB) | ||
async def create_schedule( | ||
schedule: ScheduleCreate, schedule_service: ScheduleService = Depends(get_schedule_service) | ||
): | ||
try: | ||
created_schedule = await schedule_service.create_schedule(schedule) | ||
return created_schedule | ||
except HTTPException as http_ex: | ||
raise http_ex | ||
except Exception as e: | ||
print(e) | ||
raise HTTPException(status_code=500, detail=str(e)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
from enum import Enum | ||
from uuid import UUID | ||
from datetime import datetime, timedelta | ||
from typing import List, Optional | ||
from app.schemas.time_block import TimeBlockBase, TimeBlockId, TimeBlockFull | ||
from pydantic import BaseModel, ConfigDict | ||
|
||
|
||
|
||
class ScheduleState(str, Enum): | ||
PENDING_PARTICIPANT = "PENDING_PARTICIPANT_RESPONSE" | ||
PENDING_VOLUNTEER = "PENDING_VOLUNTEER_RESPONSE" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: keep these in the same order as in the DB, just in case someone uses this as a reference for the ordering. |
||
SCHEDULED = "SCHEDULED" | ||
COMPLETED = "COMPLETED" | ||
|
||
@classmethod | ||
def to_schedule_state_id(cls, state: "ScheduleState") -> int: | ||
state_map = { | ||
cls.PENDING_VOLUNTEER: 1, | ||
cls.PENDING_PARTICIPANT: 2, | ||
cls.SCHEDULED: 3, | ||
cls.COMPLETED: 4} | ||
|
||
return state_map[state] | ||
|
||
class ScheduleBase(BaseModel): | ||
scheduled_time: Optional[datetime] | ||
duration: Optional[timedelta] | ||
state_id: int | ||
|
||
|
||
class ScheduleInDB(ScheduleBase): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would call this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could also name this ScheduleEntity and then output that if that makes more sense. |
||
id: int | ||
|
||
model_config = ConfigDict(from_attributes=True) | ||
|
||
# Provides both Schedule data and full TimeBlock data | ||
class ScheduleData(ScheduleInDB): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this case I'd call this |
||
time_blocks: List[TimeBlockFull] | ||
|
||
# List of Start and End times to Create a Schedule with | ||
class ScheduleCreate(BaseModel): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As in #7, I'd call this |
||
time_blocks: List[TimeBlockBase] | ||
|
||
class ScheduleAdd(BaseModel): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would actually call this |
||
schedule_id: UUID | ||
time_blocks: List[TimeBlockBase] | ||
|
||
class ScheduleRemove(BaseModel): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would actually call this |
||
schedule_id: UUID | ||
time_blocks: List[TimeBlockId] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
|
||
from pydantic import BaseModel | ||
from datetime import datetime | ||
from uuid import UUID | ||
|
||
class TimeBlockBase(BaseModel): | ||
start_time: datetime | ||
end_time: datetime | ||
|
||
class TimeBlockId(BaseModel): | ||
id: UUID | ||
|
||
class TimeBlockFull(TimeBlockBase, TimeBlockId): | ||
''' | ||
Combines TimeBlockBase and TimeBlockId. | ||
Represents a full time block with an ID and time range. | ||
''' | ||
pass | ||
|
||
class TimeBlockInDB(BaseModel): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Call this |
||
id: UUID | ||
schedule_id: int | ||
start_time: datetime | ||
end_time: datetime |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,7 +11,7 @@ | |
|
||
# we need to load env variables before initialization code runs | ||
from . import models # noqa: E402 | ||
from .routes import user # noqa: E402 | ||
from .routes import user, schedule # noqa: E402 | ||
from .utilities.firebase_init import initialize_firebase # noqa: E402 | ||
|
||
log = logging.getLogger("uvicorn") | ||
|
@@ -20,7 +20,7 @@ | |
@asynccontextmanager | ||
async def lifespan(_: FastAPI): | ||
log.info("Starting up...") | ||
models.run_migrations() | ||
# models.run_migrations() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Uncomment this line for now, we should have a separate PR to handle updating the migration steps. |
||
initialize_firebase() | ||
yield | ||
log.info("Shutting down...") | ||
|
@@ -30,7 +30,7 @@ async def lifespan(_: FastAPI): | |
# running-alembic-migrations-on-fastapi-startup | ||
app = FastAPI(lifespan=lifespan) | ||
app.include_router(user.router) | ||
|
||
app.include_router(schedule.router) | ||
app.include_router(email.router) | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import logging | ||
from uuid import UUID | ||
from datetime import datetime, timedelta | ||
|
||
from fastapi import HTTPException | ||
from sqlalchemy.orm import Session | ||
|
||
from app.models import Schedule, TimeBlock | ||
# from app.schemas.schedule import UserCreate, UserInDB, UserRole | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should remove these comments! |
||
# from app.schemas.time_block import UserCreate, UserInDB, UserRole | ||
from app.interfaces.schedule_service import IScheduleService | ||
from app.schemas.schedule import ( | ||
ScheduleState, | ||
ScheduleCreate, | ||
ScheduleInDB, | ||
ScheduleAdd, | ||
ScheduleData, | ||
ScheduleRemove | ||
) | ||
from app.schemas.time_block import TimeBlockBase, TimeBlockId, TimeBlockFull, TimeBlockInDB | ||
|
||
class ScheduleService(IScheduleService): | ||
def __init__(self, db: Session): | ||
self.db = db | ||
self.logger = logging.getLogger(__name__) | ||
|
||
def get_schedule_by_id(self, schedule_id): | ||
pass | ||
|
||
async def create_schedule(self, schedule: ScheduleCreate) -> ScheduleInDB: | ||
try: | ||
db_schedule = Schedule( | ||
scheduled_time=None, | ||
duration=None, | ||
state_id=ScheduleState.to_schedule_state_id("PENDING_VOLUNTEER_RESPONSE") | ||
) | ||
|
||
db_schedule.time_blocks = [] | ||
|
||
# Add time blocks to the Schedule via the relationship | ||
for tb in schedule.time_blocks: | ||
db_schedule.time_blocks.append(TimeBlock( | ||
start_time=tb.start_time, | ||
end_time=tb.end_time | ||
)) | ||
|
||
|
||
# Add the Schedule object (and its time blocks) to the session | ||
# Time Blocks are inserted into db because of SqlAlchemy relationships | ||
self.db.add(db_schedule) | ||
self.db.commit() | ||
self.db.refresh(db_schedule) | ||
|
||
return ScheduleInDB.model_validate(db_schedule) | ||
except Exception as e: | ||
self.db.rollback() | ||
self.logger.error(f"Error creating Schedule: {str(e)}") | ||
raise HTTPException(status_code=500, detail=str(e)) | ||
|
||
|
||
# CURRENTLY UNUSED | ||
async def create_time_block(self, schedule_id: int, time_block: TimeBlockBase) -> TimeBlockId: | ||
# takes a schedule id | ||
# create a time block in the db | ||
|
||
try: | ||
db_time_block = TimeBlock( | ||
schedule_id = schedule_id, | ||
start_time = time_block.start_time, | ||
end_time = time_block.end_time | ||
) | ||
|
||
|
||
self.db.add(db_time_block) | ||
self.db.commit() | ||
self.db.refresh(db_time_block) | ||
|
||
return TimeBlockId.model_validate(db_time_block) | ||
except Exception as e: | ||
self.db.rollback() | ||
self.logger.error(f"Error creating time block: {str(e)}") | ||
raise HTTPException(status_code=500, detail=str(e)) | ||
|
||
|
||
# link the schedule + the time block together | ||
pass | ||
|
||
async def add_to_schedule(self, schedule: ScheduleAdd): | ||
pass | ||
|
||
async def remove_from_schedule(self, schedule: ScheduleRemove): | ||
|
||
# GET schedule | ||
# return schedule state, time_blocks | ||
# | ||
|
||
# click on the timeblock | ||
# PUT request { | ||
# timeBlockId: ... | ||
#} | ||
pass | ||
|
||
async def select_time(self, schedule_id: int, time: datetime): | ||
|
||
# loop through each time block associated with the schedule | ||
# check if time fits within a given timeblock (+1 hour) | ||
# | ||
# if it does match, update the state of the schedule to SCHEDULED | ||
# if it doesn't match, then return an error | ||
pass | ||
|
||
async def complete_schedule(self, schedule_id: int): | ||
|
||
# update schedule state to COMPLETED | ||
pass | ||
|
||
async def get_schedule(self, schedule_id: int) -> ScheduleData: | ||
|
||
# returns a schedule | ||
pass | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that I think about this, I don't think we need this abstract class.
ScheduleService
is a standalone class.