Skip to content
Merged
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: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ POST /api/projects/
PATCH /api/projects/{project_id}
DELETE /api/projects/{project_id}

GET /api/project-types/
GET /api/labels/

GET /api/auth/matrix/login
GET /api/auth/matrix/callback
GET /api/auth/me
Expand Down
12 changes: 7 additions & 5 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware

from app.routers.project_routers import router as projects_router
from app.config import get_settings
from app.routers.auth_router import router as auth_router
from app.routers.label_router import router as labels_router
from app.routers.profile_router import router as profile_router
from app.routers.category_router import router as categories_router
from app.config import get_settings
from app.routers.project_routers import router as projects_router
from app.routers.project_type_router import router as project_types_router

settings = get_settings()
app = FastAPI(
Expand Down Expand Up @@ -41,7 +42,8 @@
app.include_router(projects_router, prefix="/api")
app.include_router(auth_router, prefix="/api")
app.include_router(profile_router, prefix="/api")
app.include_router(categories_router, prefix="/api")
app.include_router(project_types_router, prefix="/api")
app.include_router(labels_router, prefix="/api")


@app.get("/api/health", tags=["system"])
Expand Down
11 changes: 7 additions & 4 deletions backend/app/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
"""Database models loaded as a group for SQLAlchemy relationship setup."""

from .category import Category
from .auth import AuthSession
from .project import Project, ProjectCategory
from .label import Label
from .project import Project
from .project_label import ProjectLabel
from .project_type import ProjectType
from .user import User
from .profile import Profile

__all__ = [
"AuthSession",
"Category",
"Label",
"Project",
"ProjectCategory",
"ProjectLabel",
"ProjectType",
"User",
"Profile",
]
32 changes: 32 additions & 0 deletions backend/app/models/label.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from datetime import UTC, datetime
from typing import TYPE_CHECKING
from uuid import UUID, uuid4

from sqlmodel import Field, Relationship
from sqlmodel_toolkit import Model

from .project_label import ProjectLabel

if TYPE_CHECKING:
from .project import Project


def utc_now() -> datetime:
return datetime.now(UTC).replace(tzinfo=None)


class Label(Model, table=True):
"""A descriptive label that may be assigned to many projects."""

__tablename__ = "labels"

id: UUID = Field(default_factory=uuid4, primary_key=True)
name: str = Field(max_length=100, unique=True, index=True)

projects: list["Project"] = Relationship(
back_populates="labels",
link_model=ProjectLabel,
)

created_at: datetime = Field(default_factory=utc_now)
updated_at: datetime = Field(default_factory=utc_now)
31 changes: 10 additions & 21 deletions backend/app/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
from sqlmodel import Field, Relationship
from sqlmodel_toolkit import Model

from .label import Label
from .project_label import ProjectLabel
from .project_type import ProjectType

if TYPE_CHECKING:
from .category import Category
from .user import User


Expand All @@ -15,23 +18,6 @@ def utc_now() -> datetime:
return datetime.now(UTC).replace(tzinfo=None)


class ProjectCategory(Model, table=True):
"""Association table between projects and categories."""

__tablename__ = "project_categories"

project_id: UUID = Field(
foreign_key="projects.id",
primary_key=True,
index=True,
)
category_id: UUID = Field(
foreign_key="categories.id",
primary_key=True,
index=True,
)


class Project(Model, table=True):
"""A project listed in the Matrix directory."""

Expand Down Expand Up @@ -72,13 +58,16 @@ class Project(Model, table=True):

supports_e2ee: bool = Field(default=False)

owner: "User" = Relationship(
project_type_id: UUID = Field(foreign_key="project_types.id", index=True)
project_type: ProjectType = Relationship(back_populates="projects")

labels: list[Label] = Relationship(
back_populates="projects",
link_model=ProjectLabel,
)

categories: list["Category"] = Relationship(
owner: "User" = Relationship(
back_populates="projects",
link_model=ProjectCategory,
)

created_at: datetime = Field(
Expand Down
21 changes: 21 additions & 0 deletions backend/app/models/project_label.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from uuid import UUID

from sqlmodel import Field
from sqlmodel_toolkit import Model


class ProjectLabel(Model, table=True):
"""Association table between projects and descriptive labels."""

__tablename__ = "project_labels"

project_id: UUID = Field(
foreign_key="projects.id",
primary_key=True,
index=True,
)
label_id: UUID = Field(
foreign_key="labels.id",
primary_key=True,
index=True,
)
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
from datetime import UTC, datetime
from typing import TYPE_CHECKING
from uuid import UUID, uuid4

from sqlmodel import Field, Relationship
from sqlmodel_toolkit import Model
from .project import Project, ProjectCategory
from uuid import UUID, uuid4

if TYPE_CHECKING:
from .project import Project


def utc_now() -> datetime:
return datetime.now(UTC).replace(tzinfo=None)


class Category(Model, table=True):
"""A category that a project can belong to."""
class ProjectType(Model, table=True):
"""The single primary kind assigned to a project."""

__tablename__ = "categories"
__tablename__ = "project_types"

id: UUID = Field(default_factory=uuid4, primary_key=True)
name: str = Field(max_length=100, unique=True, index=True)

name: str = Field(unique=True, index=True)
projects: list["Project"] = Relationship(back_populates="project_type")

created_at: datetime = Field(default_factory=utc_now)
updated_at: datetime = Field(default_factory=utc_now)

projects: list[Project] = Relationship(
back_populates="categories",
link_model=ProjectCategory,
)
15 changes: 0 additions & 15 deletions backend/app/routers/category_router.py

This file was deleted.

16 changes: 16 additions & 0 deletions backend/app/routers/label_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from fastapi import APIRouter, Depends
from sqlmodel import Session

from app.database import get_session
from app.models.label import Label
from app.schemas.label import LabelRead
from app.services import labels_service

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


@router.get("/", response_model=list[LabelRead])
def list_labels(
session: Session = Depends(get_session),
) -> list[Label]:
return labels_service.list_labels(session)
18 changes: 14 additions & 4 deletions backend/app/routers/project_routers.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
from uuid import UUID

from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session
from uuid import UUID

from app.database import get_session
from app.dependencies import get_current_user
from app.models.project import Project
from app.models.user import User
from app.schemas.project import ProjectCreate, ProjectRead, ProjectUpdate
from app.services import projects_service
from app.services.errors import (
LabelNotFoundError,
ProjectLinkRequiredError,
ProjectTypeNotFoundError,
)

router = APIRouter(
prefix="/projects",
Expand Down Expand Up @@ -58,7 +64,10 @@ def create_project(
) -> Project:
try:
return projects_service.create_project(session, data, user_id=user.id)
except projects_service.CategoryNotFoundError as exc:
except (
LabelNotFoundError,
ProjectTypeNotFoundError,
) as exc:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(exc),
Expand All @@ -80,8 +89,9 @@ def update_project(
user_id=user.id,
)
except (
projects_service.CategoryNotFoundError,
projects_service.ProjectLinkRequiredError,
LabelNotFoundError,
ProjectLinkRequiredError,
ProjectTypeNotFoundError,
) as exc:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
Expand Down
16 changes: 16 additions & 0 deletions backend/app/routers/project_type_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from fastapi import APIRouter, Depends
from sqlmodel import Session

from app.database import get_session
from app.models.project_type import ProjectType
from app.schemas.project_type import ProjectTypeRead
from app.services import project_types_service

router = APIRouter(prefix="/project-types", tags=["project types"])


@router.get("/", response_model=list[ProjectTypeRead])
def list_project_types(
session: Session = Depends(get_session),
) -> list[ProjectType]:
return project_types_service.list_project_types(session)
15 changes: 0 additions & 15 deletions backend/app/schemas/category.py

This file was deleted.

12 changes: 12 additions & 0 deletions backend/app/schemas/label.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from uuid import UUID

from pydantic import BaseModel, ConfigDict


class LabelRead(BaseModel):
"""Public representation of a descriptive project label."""

model_config = ConfigDict(from_attributes=True)

id: UUID
name: str
Loading