Skip to content
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

Open
wants to merge 34 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5a0d435
Create schedules model
janealsh Oct 26, 2024
c8907d9
add dependencies
mmiqball Nov 3, 2024
05ac366
add pdyantic user models
mmiqball Nov 3, 2024
cf4cff6
add Firebase authentication initialization
mmiqball Nov 3, 2024
287d434
implement user creation endpoint
mmiqball Nov 3, 2024
5f8fb34
add user creation unit test
mmiqball Nov 3, 2024
b44baf9
separate user service initialization in user routes
mmiqball Nov 3, 2024
a2bc89a
simplify firebase initialization
mmiqball Nov 8, 2024
50c554e
load env before code executes
mmiqball Nov 8, 2024
a1abe9b
construct firebase service account key path from pwd
mmiqball Nov 8, 2024
637db5c
Create schedules model
janealsh Oct 26, 2024
7caddb5
Merge branch 'janealsh/LLSC-24-schedules-model' of https://github.com…
sunbagel Nov 12, 2024
5ad9a78
create TimeBlock model
sunbagel Nov 12, 2024
6a72e13
finalized schedule model
janealsh Nov 12, 2024
e52dad2
Finalize schedules model
janealsh Nov 12, 2024
c925fff
set up boilerplate code for schedules
sunbagel Nov 14, 2024
f2e74a0
create outline for schedules and schemas
sunbagel Nov 22, 2024
55d8848
update schedule model
sunbagel Nov 22, 2024
789ffc8
add create_schedule and update schemas
sunbagel Nov 22, 2024
37091d0
update Schedule and TimeBlock models to include int id
sunbagel Nov 22, 2024
4b4bd41
add alembic migration for new models
sunbagel Nov 22, 2024
4bf5014
add schedule_states initialization in alembic
sunbagel Nov 22, 2024
a845a46
add create_schedule endpoint
sunbagel Nov 24, 2024
d353aea
testing schedule endpoint
sunbagel Nov 26, 2024
c43780d
update pdm lock
sunbagel Dec 7, 2024
8c200f2
update Schedule schema
sunbagel Dec 8, 2024
c628efb
update ScheduleInDB pydantic schemas
sunbagel Dec 12, 2024
488a556
Merge branch 'main' into merging-branch
sunbagel Dec 19, 2024
c6d711c
Fix scheduleservice paths
sunbagel Dec 19, 2024
e669131
update schedule_status
sunbagel Feb 5, 2025
96a2e24
address comments
sunbagel Feb 5, 2025
b658eb9
remove schedule service interface
sunbagel Feb 12, 2025
8ccb174
update revision name
sunbagel Feb 12, 2025
76f8365
Fixing Nits (#24)
emilyniee Feb 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions backend/app/interfaces/schedule_service.py
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):
Copy link
Collaborator

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.

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


19 changes: 19 additions & 0 deletions backend/app/models/Schedule.py
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rename this to selected_time since the word schedule is being overloaded in this model.

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")
9 changes: 9 additions & 0 deletions backend/app/models/ScheduleState.py
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):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rename this to ScheduleStatus, I think this would be more indicative of a progession since Schedule is general.

__tablename__ = "schedule_states"
id = Column(Integer, primary_key=True)
name = Column(String(80), nullable=False)
18 changes: 18 additions & 0 deletions backend/app/models/TimeBlock.py
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")

5 changes: 4 additions & 1 deletion backend/app/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
from .Base import Base
from .Role import Role
from .User import User
from .Schedule import Schedule
from .ScheduleState import ScheduleState
from .TimeBlock import TimeBlock

# Used to avoid import errors for the models
__all__ = ["Base", "User", "Role"]
__all__ = ["Base", "User", "Role", "Schedule", "ScheduleState", "TimeBlock" ]


def run_migrations():
Expand Down
32 changes: 32 additions & 0 deletions backend/app/routes/schedule.py
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))

52 changes: 52 additions & 0 deletions backend/app/schemas/schedule.py
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"
Copy link
Collaborator

Choose a reason for hiding this comment

The 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):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would call this ScheduleResponse or ScheduleCreateResponse, depending if you think it will be unique to the CREATE schedule request. the Schedule object is already a representation of the schedule in the DB, and we don't always want a full representation of what's in the DB in our output, so I don't want to signal that, eg. some places just output id instead of id properties in their API responses.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case I'd call this ScheduleGetResponse.

time_blocks: List[TimeBlockFull]

# List of Start and End times to Create a Schedule with
class ScheduleCreate(BaseModel):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in #7, I'd call this ScheduleCreateRequest.

time_blocks: List[TimeBlockBase]

class ScheduleAdd(BaseModel):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would actually call this ScheduleUpdateRequest too, just to follow REST principles, especially because Add can be confused with create with the word Schedule.

schedule_id: UUID
time_blocks: List[TimeBlockBase]

class ScheduleRemove(BaseModel):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would actually call this ScheduleDeleteRequest too, just to follow REST principles.

schedule_id: UUID
time_blocks: List[TimeBlockId]

24 changes: 24 additions & 0 deletions backend/app/schemas/time_block.py
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):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call this TimeBlockEntity instead, InDB isn't standard for DB representations.

id: UUID
schedule_id: int
start_time: datetime
end_time: datetime
6 changes: 3 additions & 3 deletions backend/app/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -20,7 +20,7 @@
@asynccontextmanager
async def lifespan(_: FastAPI):
log.info("Starting up...")
models.run_migrations()
# models.run_migrations()
Copy link
Collaborator

Choose a reason for hiding this comment

The 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...")
Expand All @@ -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)


Expand Down
122 changes: 122 additions & 0 deletions backend/app/services/implementations/schedule_service.py
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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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


4 changes: 2 additions & 2 deletions backend/migrations/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@

# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# if config.config_file_name is not None:
sunbagel marked this conversation as resolved.
Show resolved Hide resolved
# fileConfig(config.config_file_name)

# add your model's MetaData object here
# for 'autogenerate' support
Expand Down
Loading
Loading