Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 32 additions & 0 deletions api_schemas/tool_booking_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from typing import Annotated
from api_schemas.tool_schema import SimpleToolRead
from api_schemas.user_schemas import SimpleUserRead
from helpers.types import datetime_utc
from pydantic import StringConstraints
from api_schemas.base_schema import BaseSchema
from helpers.constants import MAX_TOOL_BOOKING_DESC


class ToolBookingCreate(BaseSchema):
tool_id: int
amount: int
start_time: datetime_utc
end_time: datetime_utc
description: Annotated[str, StringConstraints(max_length=MAX_TOOL_BOOKING_DESC)]


class ToolBookingRead(BaseSchema):
id: int
tool: SimpleToolRead
amount: int
user: SimpleUserRead
start_time: datetime_utc
end_time: datetime_utc
description: str


class ToolBookingUpdate(BaseSchema):
amount: int | None = None
start_time: datetime_utc | None = None
end_time: datetime_utc | None = None
description: Annotated[str, StringConstraints(max_length=MAX_TOOL_BOOKING_DESC)] | None = None
35 changes: 35 additions & 0 deletions api_schemas/tool_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from api_schemas.base_schema import BaseSchema
from typing import Annotated
from pydantic import StringConstraints

from helpers.constants import MAX_TOOL_DESC


class ToolCreate(BaseSchema):
name_sv: str
name_en: str
amount: int
description_sv: Annotated[str, StringConstraints(max_length=MAX_TOOL_DESC)] | None = None
description_en: Annotated[str, StringConstraints(max_length=MAX_TOOL_DESC)] | None = None


class ToolRead(BaseSchema):
id: int
name_sv: str
name_en: str
amount: int
description_sv: str | None
description_en: str | None


class ToolUpdate(BaseSchema):
name_sv: str
name_en: str
amount: int
description_sv: Annotated[str, StringConstraints(max_length=MAX_TOOL_DESC)] | None = None
description_en: Annotated[str, StringConstraints(max_length=MAX_TOOL_DESC)] | None = None


class SimpleToolRead(BaseSchema):
id: int
amount: int
31 changes: 31 additions & 0 deletions db_models/tool_booking_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from helpers.constants import MAX_TOOL_BOOKING_DESC
from .base_model import BaseModel_DB
from sqlalchemy.orm import mapped_column, Mapped, relationship
from typing import TYPE_CHECKING, Optional
from sqlalchemy import ForeignKey, String
from helpers.types import datetime_utc

if TYPE_CHECKING:
from .user_model import User_DB
from .tool_model import Tool_DB


class ToolBooking_DB(BaseModel_DB):
__tablename__ = "tool_booking_table"

id: Mapped[int] = mapped_column(primary_key=True, init=False)

amount: Mapped[int] = mapped_column()

start_time: Mapped[datetime_utc] = mapped_column()
end_time: Mapped[datetime_utc] = mapped_column()

tool_id: Mapped[int] = mapped_column(ForeignKey("tool_table.id"))
tool: Mapped["Tool_DB"] = relationship(back_populates="bookings", init=False)

user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("user_table.id"))
user: Mapped[Optional["User_DB"]] = relationship(back_populates="tool_bookings", init=False)

description: Mapped[Optional[str]] = mapped_column(String(MAX_TOOL_BOOKING_DESC), default=None)

pass
29 changes: 29 additions & 0 deletions db_models/tool_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from helpers.constants import MAX_TOOL_NAME, MAX_TOOL_DESC
from .base_model import BaseModel_DB
from .tool_booking_model import ToolBooking_DB
from sqlalchemy.orm import mapped_column, Mapped, relationship
from typing import TYPE_CHECKING, Optional
from sqlalchemy import String, Integer

if TYPE_CHECKING:
from .tool_booking_model import ToolBooking_DB


class Tool_DB(BaseModel_DB):
__tablename__ = "tool_table"

id: Mapped[int] = mapped_column(primary_key=True, init=False)

name_sv: Mapped[str] = mapped_column(String(MAX_TOOL_NAME))
name_en: Mapped[str] = mapped_column(String(MAX_TOOL_NAME))

amount: Mapped[int] = mapped_column(Integer)

bookings: Mapped[list["ToolBooking_DB"]] = relationship(
back_populates="tool", cascade="all, delete-orphan", init=False
)

description_sv: Mapped[Optional[str]] = mapped_column(String(MAX_TOOL_DESC), default=None)
description_en: Mapped[Optional[str]] = mapped_column(String(MAX_TOOL_DESC), default=None)

pass
6 changes: 6 additions & 0 deletions db_models/user_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from helpers.types import datetime_utc
from .ad_model import BookAd_DB
from .car_booking_model import CarBooking_DB
from .tool_booking_model import ToolBooking_DB
from helpers.types import datetime_utc

if TYPE_CHECKING:
Expand All @@ -29,6 +30,7 @@
from .news_model import News_DB
from .ad_model import BookAd_DB
from .cafe_shift_model import CafeShift_DB
from .tool_booking_model import ToolBooking_DB


# called by SQLAlchemy when user.posts.append(some_post)
Expand Down Expand Up @@ -91,6 +93,10 @@ class User_DB(BaseModel_DB, SQLAlchemyBaseUserTable[int]):

cafe_shifts: Mapped[list["CafeShift_DB"]] = relationship(back_populates="user", init=False)

tool_bookings: Mapped[list["ToolBooking_DB"]] = relationship(
back_populates="user", cascade="all, delete-orphan", passive_deletes=True, init=False
)

accesses: Mapped[list["UserDoorAccess_DB"]] = relationship(
back_populates="user", cascade="all, delete-orphan", init=False
)
Expand Down
5 changes: 5 additions & 0 deletions helpers/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,8 @@
MAX_GUILD_MEETING_DATE_DESC = 500
MAX_GUILD_MEETING_DESC = 10000
MAX_GUILD_MEETING_TITLE = 200

# Tool booking
MAX_TOOL_NAME = 100
MAX_TOOL_DESC = 1000
MAX_TOOL_BOOKING_DESC = 1000
2 changes: 2 additions & 0 deletions helpers/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ def force_utc(date: datetime):
"Moosegame",
"MailAlias",
"GuildMeeting",
"Tools",
"ToolBookings",
]

# This is a little ridiculous now, but if we have many actions, this is a neat system.
Expand Down
6 changes: 6 additions & 0 deletions routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
from .sub_election_router import sub_election_router
from .nomination_router import nomination_router
from .guild_meeting_router import guild_meeting_router
from .tool_router import tool_router
from .tool_booking_router import tool_booking_router

# here comes the big momma router
main_router = APIRouter()
Expand Down Expand Up @@ -94,3 +96,7 @@
main_router.include_router(nomination_router, prefix="/nominations", tags=["nominations"])

main_router.include_router(guild_meeting_router, prefix="/guild-meeting", tags=["guild meeting"])

main_router.include_router(tool_router, prefix="/tools", tags=["tools"])

main_router.include_router(tool_booking_router, prefix="/tool-booking", tags=["tool booking"])
183 changes: 183 additions & 0 deletions routes/tool_booking_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
from fastapi import APIRouter, HTTPException
from sqlalchemy import and_
from api_schemas.tool_booking_schema import (
ToolBookingCreate,
ToolBookingRead,
ToolBookingUpdate,
)
from database import DB_dependency
from typing import Annotated
from db_models.tool_model import Tool_DB
from user.permission import Permission
from db_models.user_model import User_DB
from db_models.tool_booking_model import ToolBooking_DB
from helpers.types import datetime_utc
from services import tool_booking_service


tool_booking_router = APIRouter()


@tool_booking_router.post(
"/", response_model=ToolBookingRead, dependencies=[Permission.require("manage", "ToolBookings")]
)
def create_tool_booking(
data: ToolBookingCreate,
current_user: Annotated[User_DB, Permission.require("manage", "ToolBookings")],
db: DB_dependency,
):
tool = db.query(Tool_DB).filter(Tool_DB.id == data.tool_id).one_or_none()
if tool is None:
raise HTTPException(404, "Tool not found")

if data.amount <= 0:
raise HTTPException(400, "Amount must be positive")

if data.end_time <= data.start_time:
raise HTTPException(400, "End time must be after start time")

overlapping_bookings = (
db.query(ToolBooking_DB)
.filter(
and_(
ToolBooking_DB.tool_id == data.tool_id,
ToolBooking_DB.start_time < data.end_time,
data.start_time < ToolBooking_DB.end_time,
)
)
.all()
)

booked_amount = tool_booking_service.max_booked(overlapping_bookings)

if booked_amount + data.amount > tool.amount:
raise HTTPException(400, "Not enough tools available at that time")

tool_booking = ToolBooking_DB(
tool_id=data.tool_id,
amount=data.amount,
start_time=data.start_time,
end_time=data.end_time,
user_id=current_user.id,
description=data.description,
)

db.add(tool_booking)

db.commit()

return tool_booking


@tool_booking_router.get(
"/get_booking/{booking_id}",
response_model=ToolBookingRead,
dependencies=[Permission.require("view", "ToolBookings")],
)
def get_tool_booking(booking_id: int, db: DB_dependency):
booking = db.query(ToolBooking_DB).filter(ToolBooking_DB.id == booking_id).one_or_none()
if booking is None:
raise HTTPException(404, "Tool booking not found")
return booking


@tool_booking_router.get(
"/get_all",
response_model=list[ToolBookingRead],
dependencies=[Permission.require("view", "ToolBookings")],
)
def get_all_tool_bookings(db: DB_dependency):
bookings = db.query(ToolBooking_DB).all()
return bookings


@tool_booking_router.get(
"/get_between_times",
response_model=list[ToolBookingRead],
dependencies=[Permission.require("view", "ToolBookings")],
)
def get_tool_bookings_between_times(db: DB_dependency, start_time: datetime_utc, end_time: datetime_utc):
bookings = (
db.query(ToolBooking_DB)
.filter(and_(ToolBooking_DB.start_time >= start_time, ToolBooking_DB.end_time <= end_time))
.all()
)
return bookings


@tool_booking_router.get(
"/get_by_tool/",
response_model=list[ToolBookingRead],
dependencies=[Permission.require("view", "ToolBookings")],
)
def get_tool_bookings_by_tool(tool_id: int, db: DB_dependency):
tool = db.query(Tool_DB).filter(Tool_DB.id == tool_id).one_or_none()
if tool is None:
raise HTTPException(404, "Tool not found")
bookings = tool.bookings
return bookings


@tool_booking_router.delete(
"/{booking_id}", response_model=ToolBookingRead, dependencies=[Permission.require("manage", "ToolBookings")]
)
def remove_tool_booking(
booking_id: int,
db: DB_dependency,
):
booking = db.query(ToolBooking_DB).filter(ToolBooking_DB.id == booking_id).one_or_none()
if booking is None:
raise HTTPException(404, "Tool booking not found")

db.delete(booking)
db.commit()
return booking


@tool_booking_router.patch(
"/{booking_id}", response_model=ToolBookingRead, dependencies=[Permission.require("manage", "ToolBookings")]
)
def update_tool_booking(
booking_id: int,
data: ToolBookingUpdate,
db: DB_dependency,
):
tool_booking = db.query(ToolBooking_DB).filter(ToolBooking_DB.id == booking_id).one_or_none()
if tool_booking is None:
raise HTTPException(404, "Tool booking not found")

if data.start_time is None:
data.start_time = tool_booking.start_time
if data.end_time is None:
data.end_time = tool_booking.end_time
if data.end_time <= data.start_time:
raise HTTPException(400, "End time must be after start time")

if data.amount is not None:
if data.amount <= 0:
raise HTTPException(400, "Amount must be positive")

overlapping_bookings = (
db.query(ToolBooking_DB)
.filter(
and_(
ToolBooking_DB.id != booking_id,
ToolBooking_DB.tool_id == tool_booking.tool_id,
ToolBooking_DB.start_time < data.end_time,
data.start_time < ToolBooking_DB.end_time,
)
)
.all()
)

booked_amount = tool_booking_service.max_booked(overlapping_bookings)

if booked_amount + data.amount > tool_booking.tool.amount:
raise HTTPException(400, "Not enough tools available at that time")

for var, value in vars(data).items():
setattr(tool_booking, var, value) if value else None

db.commit()
db.refresh(tool_booking)
return tool_booking
Loading