Skip to content

Commit

Permalink
Add setup for tasks card
Browse files Browse the repository at this point in the history
  • Loading branch information
uittenbroekrobbert committed Jun 3, 2024
1 parent ccc8dd5 commit ac57364
Show file tree
Hide file tree
Showing 73 changed files with 5,824 additions and 162 deletions.
1 change: 1 addition & 0 deletions .devcontainer/postCreateCommand.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

pipx install poetry
poetry install
poetry run playwright install --with-deps
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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ jobs:
- name: Install dependencies
run: poetry install

- name: Install Playwright browsers
run: poetry run playwright install --with-deps

- name: run ruff
run: poetry run ruff check --output-format=github

Expand Down Expand Up @@ -92,6 +95,9 @@ jobs:
- name: Install dependencies
run: poetry install

- name: Install Playwright browsers
run: poetry run playwright install --with-deps

- name: Run pytest
run: poetry run coverage run -m pytest

Expand Down
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
444 changes: 331 additions & 113 deletions poetry.lock

Large diffs are not rendered by default.

18 changes: 13 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,15 @@ 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]
pytest = "^8.2.1"
coverage = "^7.5.3"
httpx = "^0.27.0"
urllib3 = "^2.2.1"

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

[tool.coverage.run]
Expand Down Expand Up @@ -110,12 +114,16 @@ filterwarnings = [
level = "PARANOID"
dependencies = true
authorized_licenses = [
"Apache Software",
"Artistic",
"BSD",
"Python Software Foundation",
"GNU General Public License v2 or later (GPLv2+)",
"GNU General Public License (GPL)",
"GNU Library or Lesser General Public License (LGPL)",
"MIT",
"Apache Software",
"GNU Library or Lesser General Public License (LGPL)",
"Mozilla Public License 2.0 (MPL 2.0)",
"The Unlicense (Unlicense)",
"ISC License (ISCL)"
"ISC License (ISCL)",
"Mozilla Public License 2.0 (MPL 2.0)",
"Python Software Foundation"
]
2 changes: 1 addition & 1 deletion sonar-project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ sonar.python.version=3.10,3.11,3.12

sonar.python.coverage.reportPaths=coverage.xml

sonar.coverage.exclusions=tad/migrations/**
sonar.coverage.exclusions=tad/migrations/**,tad/site/static/js/tad.js
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)
10 changes: 8 additions & 2 deletions tad/api/routes/root.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from fastapi.responses import FileResponse, HTMLResponse

from tad.api.deps import templates
from tad.core.config import settings
from tad.repositories.deps import templates

router = APIRouter()


@router.get("/")
async def base(request: Request) -> HTMLResponse:
return templates.TemplateResponse(request=request, name="root/index.html")


@router.get("/favicon.ico", include_in_schema=False)
async def favicon():
return FileResponse(settings.STATIC_DIR + "/favicon.ico")
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.
Loading

0 comments on commit ac57364

Please sign in to comment.