Skip to content
Closed
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
3 changes: 2 additions & 1 deletion backend/app/api/main.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from fastapi import APIRouter

from app.api.routes import items, login, private, users, utils
from app.api.routes import items, login, meetings, private, users, utils
from app.core.config import settings

api_router = APIRouter()
api_router.include_router(login.router)
api_router.include_router(users.router)
api_router.include_router(utils.router)
api_router.include_router(items.router)
api_router.include_router(meetings.router)


if settings.ENVIRONMENT == "local":
Expand Down
95 changes: 95 additions & 0 deletions backend/app/api/routes/meetings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from typing import Any
from uuid import UUID

from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session

from app import crud
from app.api import deps
from app.models import (
Meeting,
MeetingCreate,
MeetingUpdate,
MeetingPublic,
MeetingsPublic,
)

router = APIRouter(prefix="/meetings", tags=["meetings"])


@router.get("", response_model=MeetingsPublic)
def read_meetings(
*,
session: Session = Depends(deps.get_session),
skip: int = 0,
limit: int = 100,
) -> Any:
"""
Retrieve meetings.
"""
meetings, total = crud.meeting.get_meetings(
session=session, skip=skip, limit=limit
)
return MeetingsPublic(data=meetings, count=total)


@router.post("", response_model=MeetingPublic)
def create_meeting(
*,
session: Session = Depends(deps.get_session),
meeting_in: MeetingCreate,
) -> Any:
"""
Create new meeting.
"""
meeting = crud.meeting.create_meeting(session=session, meeting_in=meeting_in)
return meeting


@router.get("/{meeting_id}", response_model=MeetingPublic)
def read_meeting(
*,
session: Session = Depends(deps.get_session),
meeting_id: UUID,
) -> Any:
"""
Get meeting by ID.
"""
meeting = crud.meeting.get_meeting(session=session, meeting_id=meeting_id)
if not meeting:
raise HTTPException(status_code=404, detail="Meeting not found")
return meeting


@router.put("/{meeting_id}", response_model=MeetingPublic)
def update_meeting(
*,
session: Session = Depends(deps.get_session),
meeting_id: UUID,
meeting_in: MeetingUpdate,
) -> Any:
"""
Update a meeting.
"""
meeting = crud.meeting.get_meeting(session=session, meeting_id=meeting_id)
if not meeting:
raise HTTPException(status_code=404, detail="Meeting not found")
meeting = crud.meeting.update_meeting(
session=session, db_obj=meeting, obj_in=meeting_in
)
return meeting


@router.delete("/{meeting_id}", response_model=MeetingPublic)
def delete_meeting(
*,
session: Session = Depends(deps.get_session),
meeting_id: UUID,
) -> Any:
"""
Delete a meeting.
"""
meeting = crud.meeting.delete_meeting(session=session, meeting_id=meeting_id)
if not meeting:
raise HTTPException(status_code=404, detail="Meeting not found")
return meeting
43 changes: 42 additions & 1 deletion backend/app/crud.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import uuid
from typing import Any
from uuid import UUID

from sqlmodel import Session, select

from app.core.security import get_password_hash, verify_password
from app.models import Item, ItemCreate, User, UserCreate, UserUpdate
from app.models import Item, ItemCreate, User, UserCreate, UserUpdate, Meeting, MeetingCreate, MeetingUpdate


def create_user(*, session: Session, user_create: UserCreate) -> User:
Expand Down Expand Up @@ -52,3 +53,43 @@ def create_item(*, session: Session, item_in: ItemCreate, owner_id: uuid.UUID) -
session.commit()
session.refresh(db_item)
return db_item


def create_meeting(*, session: Session, meeting_in: MeetingCreate) -> Meeting:
db_obj = Meeting.model_validate(meeting_in)
session.add(db_obj)
session.commit()
session.refresh(db_obj)
return db_obj


def get_meeting(*, session: Session, meeting_id: UUID) -> Meeting | None:
return session.get(Meeting, meeting_id)


def get_meetings(
*, session: Session, skip: int = 0, limit: int = 100
) -> tuple[list[Meeting], int]:
statement = select(Meeting).offset(skip).limit(limit)
meetings = session.exec(statement).all()
total = session.query(Meeting).count()
return meetings, total


def update_meeting(
*, session: Session, db_obj: Meeting, obj_in: MeetingUpdate
) -> Meeting:
update_data = obj_in.model_dump(exclude_unset=True)
db_obj.sqlmodel_update(update_data)
session.add(db_obj)
session.commit()
session.refresh(db_obj)
return db_obj


def delete_meeting(*, session: Session, meeting_id: UUID) -> Meeting | None:
meeting = session.get(Meeting, meeting_id)
if meeting:
session.delete(meeting)
session.commit()
return meeting
33 changes: 33 additions & 0 deletions backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,36 @@ class TokenPayload(SQLModel):
class NewPassword(SQLModel):
token: str
new_password: str = Field(min_length=8, max_length=40)


# Shared properties
class MeetingBase(SQLModel):
title: str = Field(min_length=1, max_length=255)
agenda: str = Field(min_length=1)
summary: str | None = Field(default=None)


# Properties to receive on meeting creation
class MeetingCreate(MeetingBase):
pass


# Properties to receive on meeting update
class MeetingUpdate(MeetingBase):
title: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore
agenda: str | None = Field(default=None, min_length=1) # type: ignore


# Database model
class Meeting(MeetingBase, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)


# Properties to return via API
class MeetingPublic(MeetingBase):
id: uuid.UUID


class MeetingsPublic(SQLModel):
data: list[MeetingPublic]
count: int
120 changes: 120 additions & 0 deletions backend/app/tests/api/routes/test_meetings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import uuid

from fastapi.testclient import TestClient
from sqlmodel import Session

from app.core.config import settings
from app.tests.utils.meeting import create_random_meeting


def test_create_meeting(
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
data = {"title": "Test Meeting", "agenda": "Discuss testing"}
response = client.post(
f"{settings.API_V1_STR}/meetings/",
headers=superuser_token_headers,
json=data,
)
assert response.status_code == 200
content = response.json()
assert content["title"] == data["title"]
assert content["agenda"] == data["agenda"]
assert "id" in content


def test_read_meeting(
client: TestClient, superuser_token_headers: dict[str, str], db: Session
) -> None:
meeting = create_random_meeting(db)
response = client.get(
f"{settings.API_V1_STR}/meetings/{meeting.id}",
headers=superuser_token_headers,
)
assert response.status_code == 200
content = response.json()
assert content["title"] == meeting.title
assert content["agenda"] == meeting.agenda
assert content["id"] == str(meeting.id)


def test_read_meeting_not_found(
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
response = client.get(
f"{settings.API_V1_STR}/meetings/{uuid.uuid4()}",
headers=superuser_token_headers,
)
assert response.status_code == 404
content = response.json()
assert content["detail"] == "Meeting not found"


def test_read_meetings(
client: TestClient, superuser_token_headers: dict[str, str], db: Session
) -> None:
create_random_meeting(db)
create_random_meeting(db)
response = client.get(
f"{settings.API_V1_STR}/meetings/",
headers=superuser_token_headers,
)
assert response.status_code == 200
content = response.json()
assert len(content["data"]) >= 2


def test_update_meeting(
client: TestClient, superuser_token_headers: dict[str, str], db: Session
) -> None:
meeting = create_random_meeting(db)
data = {"title": "Updated Meeting", "agenda": "Updated agenda"}
response = client.put(
f"{settings.API_V1_STR}/meetings/{meeting.id}",
headers=superuser_token_headers,
json=data,
)
assert response.status_code == 200
content = response.json()
assert content["title"] == data["title"]
assert content["agenda"] == data["agenda"]
assert content["id"] == str(meeting.id)


def test_update_meeting_not_found(
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
data = {"title": "Updated Meeting", "agenda": "Updated agenda"}
response = client.put(
f"{settings.API_V1_STR}/meetings/{uuid.uuid4()}",
headers=superuser_token_headers,
json=data,
)
assert response.status_code == 404
content = response.json()
assert content["detail"] == "Meeting not found"


def test_delete_meeting(
client: TestClient, superuser_token_headers: dict[str, str], db: Session
) -> None:
meeting = create_random_meeting(db)
response = client.delete(
f"{settings.API_V1_STR}/meetings/{meeting.id}",
headers=superuser_token_headers,
)
assert response.status_code == 200
content = response.json()
assert content["id"] == str(meeting.id)


def test_delete_meeting_not_found(
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
response = client.delete(
f"{settings.API_V1_STR}/meetings/{uuid.uuid4()}",
headers=superuser_token_headers,
)
assert response.status_code == 404
content = response.json()
assert content["detail"] == "Meeting not found"
18 changes: 18 additions & 0 deletions backend/app/tests/utils/meeting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import random
import string

from sqlmodel import Session

from app import crud
from app.models import MeetingCreate


def random_lower_string() -> str:
return "".join(random.choices(string.ascii_lowercase, k=32))


def create_random_meeting(db: Session) -> None:
title = random_lower_string()
agenda = random_lower_string()
meeting_in = MeetingCreate(title=title, agenda=agenda)
return crud.meeting.create_meeting(session=db, meeting_in=meeting_in)
Loading