Skip to content

Commit

Permalink
# This is a combination of 2 commits.
Browse files Browse the repository at this point in the history
# This is the 1st commit message:

Add setup for tasks card

# This is the commit message #2:

Rewrite database test functions to a class with a fixture
  • Loading branch information
uittenbroekrobbert committed Jun 3, 2024
1 parent ccc8dd5 commit 5507b2c
Show file tree
Hide file tree
Showing 69 changed files with 5,448 additions and 40 deletions.
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ PROJECT_NAME="TAD"
# TAD backend
BACKEND_CORS_ORIGINS="http://localhost,https://localhost,http://127.0.0.1,https://127.0.0.1"
SECRET_KEY=changethis
APP_DATABASE_SCHEME="postgresql"
APP_DATABASE_SCHEME="sqlite"
APP_DATABASE_USER=tad
APP_DATABASE_DB=tad
APP_DATABASE_PASSWORD=changethis
Expand Down
2 changes: 2 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ POSTGRES_PASSWORD=changethis

# Database viewer
PGADMIN_DEFAULT_PASSWORD=changethis

APP_DATABASE_FILE=database.sqlite3
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ __pypackages__/

#mypyr
.mypy_cache/
/.idea/

# macos
.DS_Store
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ jinja2 = "^3.1.4"
pydantic-settings = "^2.2.1"
psycopg2-binary = "^2.9.9"
uvicorn = {extras = ["standard"], version = "^0.30.1"}
playwright = "^1.44.0"
pytest-playwright = "^0.5.0"


[tool.poetry.group.test.dependencies]
Expand Down Expand Up @@ -77,7 +79,8 @@ reportMissingImports = true
reportMissingTypeStubs = true
reportUnnecessaryIsInstance = false
exclude = [
"tad/migrations"
"tad/migrations",
".venv"
]

[tool.coverage.run]
Expand Down
4 changes: 3 additions & 1 deletion tad/api/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from fastapi import APIRouter

from tad.api.routes import health, root
from tad.api.routes import health, pages, root, tasks

api_router = APIRouter()
api_router.include_router(root.router)
api_router.include_router(health.router, prefix="/health", tags=["health"])
api_router.include_router(pages.router, prefix="/pages", tags=["pages"])
api_router.include_router(tasks.router, prefix="/tasks", tags=["tasks"])
25 changes: 25 additions & 0 deletions tad/api/routes/pages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from typing import Annotated

from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates

from tad.services.statuses import StatusesService
from tad.services.tasks import TasksService

router = APIRouter()
templates = Jinja2Templates(directory="tad/site/templates")


@router.get("/", response_class=HTMLResponse)
async def default_layout(
request: Request,
status_service: Annotated[StatusesService, Depends(StatusesService)],
tasks_service: Annotated[TasksService, Depends(TasksService)],
):
context = {
"page_title": "This is the page title",
"tasks_service": tasks_service,
"statuses_service": status_service,
}
return templates.TemplateResponse(request=request, name="default_layout.jinja", context=context)
2 changes: 1 addition & 1 deletion tad/api/routes/root.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse

from tad.api.deps import templates
from tad.repositories.deps import templates

router = APIRouter()

Expand Down
48 changes: 48 additions & 0 deletions tad/api/routes/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from typing import Annotated, Any

from fastapi import APIRouter, Depends, Request, status
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates

from tad.models.task import MoveTask
from tad.services.tasks import TasksService

router = APIRouter()
templates = Jinja2Templates(directory="tad/site/templates")


@router.post("/move", response_class=HTMLResponse)
async def move_task(
request: Request, move_task: MoveTask, tasks_service: Annotated[TasksService, Depends(TasksService)]
) -> HTMLResponse:
"""
Move a task through an API call.
:param tasks_service: the task service
:param request: the request object
:param move_task: the move task object
:return: a HTMLResponse object, in this case the html code of the card that was moved
"""
try:
task = tasks_service.move_task(
convert_to_int_if_is_int(move_task.id),
convert_to_int_if_is_int(move_task.status_id),
convert_to_int_if_is_int(move_task.previous_sibling_id),
convert_to_int_if_is_int(move_task.next_sibling_id),
)
# todo(Robbert) add error handling for input error or task error handling
return templates.TemplateResponse(request=request, name="task.jinja", context={"task": task})
except Exception:
return templates.TemplateResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, request=request, name="error.jinja"
)


def convert_to_int_if_is_int(value: Any) -> int | Any:
"""
If the given value is of type int, return it as int, otherwise return the input value as is.
:param value: the value to convert
:return: the value as int or the original type
"""
if value is not None and isinstance(value, str) and value.isdigit():
return int(value)
return value
6 changes: 4 additions & 2 deletions tad/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
# Self type is not available in Python 3.10 so create our own with TypeVar
SelfSettings = TypeVar("SelfSettings", bound="Settings")

logger = logging.getLogger(__name__)


class Settings(BaseSettings):
# todo(berry): investigate yaml, toml or json file support for SettingsConfigDict
Expand Down Expand Up @@ -42,8 +44,8 @@ def server_host(self) -> str:
PROJECT_NAME: str = "TAD"
PROJECT_DESCRIPTION: str = "Transparency of Algorithmic Decision making"

STATIC_DIR: str = "tad/static"
TEMPLATE_DIR: str = "tad/templates"
STATIC_DIR: str = "tad/site/static/"
TEMPLATE_DIR: str = "tad/site/templates"

# todo(berry): create submodel for database settings
APP_DATABASE_SCHEME: DatabaseSchemaType = "sqlite"
Expand Down
12 changes: 10 additions & 2 deletions tad/core/db.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
from sqlalchemy.engine.base import Engine
from sqlmodel import Session, create_engine, select

from tad.core.config import settings

engine = create_engine(settings.SQLALCHEMY_DATABASE_URI)
_engine: None | Engine = None


def get_engine() -> Engine:
global _engine
if _engine is None:
_engine = create_engine(settings.SQLALCHEMY_DATABASE_URI)
return _engine


async def check_db():
with Session(engine) as session:
with Session(get_engine()) as session:
session.exec(select(1))
7 changes: 5 additions & 2 deletions tad/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@
validation_exception_handler as tad_validation_exception_handler,
)
from tad.core.log import configure_logging
from tad.middleware.route_logging import RequestLoggingMiddleware
from tad.utils.mask import Mask

from .middleware.route_logging import RequestLoggingMiddleware

configure_logging(settings.LOGGING_LEVEL, settings.LOGGING_CONFIG)


logger = logging.getLogger(__name__)
mask = Mask(mask_keywords=["database_uri"])

Expand Down Expand Up @@ -52,7 +54,6 @@ async def lifespan(app: FastAPI):
)

app.add_middleware(RequestLoggingMiddleware)

app.mount("/static", StaticFiles(directory=settings.STATIC_DIR), name="static")


Expand All @@ -67,3 +68,5 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE


app.include_router(api_router)

# todo (robbert) add init code for example tasks and statuses
67 changes: 67 additions & 0 deletions tad/migrations/versions/eb2eed884ae9_a_message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""a message
Revision ID: eb2eed884ae9
Revises:
Create Date: 2024-05-14 13:36:23.551663
"""

from collections.abc import Sequence

import sqlalchemy as sa
import sqlmodel.sql.sqltypes
from alembic import op

# revision identifiers, used by Alembic.
revision: str = "eb2eed884ae9"
down_revision: str | None = None
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"hero",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"status",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("sort_order", sa.Float(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"user",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("avatar", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"task",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("title", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("sort_order", sa.Float(), nullable=False),
sa.Column("status_id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(
["user_id"],
["user.id"],
),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("task")
op.drop_table("user")
op.drop_table("status")
op.drop_table("hero")
# ### end Alembic commands ###
5 changes: 4 additions & 1 deletion tad/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from .hero import Hero
from .status import Status
from .task import Task
from .user import User

__all__ = ["Hero"]
__all__ = ["Hero", "Task", "Status", "User"]
7 changes: 7 additions & 0 deletions tad/models/status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from sqlmodel import Field, SQLModel # type: ignore


class Status(SQLModel, table=True):
id: int = Field(default=None, primary_key=True)
name: str
sort_order: float
30 changes: 30 additions & 0 deletions tad/models/task.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from pydantic import BaseModel, ValidationInfo, field_validator
from pydantic import Field as PydanticField # type: ignore
from sqlmodel import Field as SQLField # type: ignore
from sqlmodel import SQLModel


class Task(SQLModel, table=True):
id: int = SQLField(default=None, primary_key=True)
title: str
description: str
sort_order: float
status_id: int | None = SQLField(default=None, foreign_key="status.id")
user_id: int | None = SQLField(default=None, foreign_key="user.id")
# todo(robbert) Tasks probably are grouped (and sub-grouped), so we probably need a reference to a group_id


class MoveTask(BaseModel):
# todo(robbert) values from htmx json are all strings, using type int does not work for
# sibling variables (they are optional)
id: str = PydanticField(None, alias="taskId", strict=False)
status_id: str = PydanticField(None, alias="statusId", strict=False)
previous_sibling_id: str | None = PydanticField(None, alias="previousSiblingId", strict=False)
next_sibling_id: str | None = PydanticField(None, alias="nextSiblingId", strict=False)

@field_validator("id", "status_id", "previous_sibling_id", "next_sibling_id")
@classmethod
def check_is_int(cls, value: str, info: ValidationInfo) -> str:
if isinstance(value, str) and value.isdigit():
assert value.isdigit(), f"{info.field_name} must be an integer" # noqa: S101
return value
7 changes: 7 additions & 0 deletions tad/models/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from sqlmodel import Field, SQLModel # type: ignore


class User(SQLModel, table=True):
id: int = Field(default=None, primary_key=True)
name: str
avatar: str | None
File renamed without changes.
11 changes: 3 additions & 8 deletions tad/api/deps.py → tad/repositories/deps.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
from collections.abc import Generator
from typing import Annotated

from fastapi import Depends
from fastapi.templating import Jinja2Templates
from sqlmodel import Session

from tad.core.config import settings
from tad.core.db import engine
from tad.core.db import get_engine

templates = Jinja2Templates(directory=settings.TEMPLATE_DIR)


def get_db() -> Generator[Session, None, None]:
with Session(engine) as session:
def get_session() -> Generator[Session, None, None]:
with Session(get_engine()) as session:
yield session


SessionDep = Annotated[Session, Depends(get_db)]
8 changes: 8 additions & 0 deletions tad/repositories/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from tad.core.exceptions import TADError


class RepositoryError(TADError):
def __init__(self, message: str = "Repository error"):
self.message: str = message
exception_name: str = self.__class__.__name__
super().__init__(f"{exception_name}: {self.message}")
Loading

0 comments on commit 5507b2c

Please sign in to comment.