diff --git a/tad/api/deps.py b/tad/api/deps.py index 1a7c8622..a4bca175 100644 --- a/tad/api/deps.py +++ b/tad/api/deps.py @@ -15,5 +15,7 @@ def get_db() -> Generator[Session, None, None]: with Session(engine) as session: yield session + session.get() + SessionDep = Annotated[Session, Depends(get_db)] diff --git a/tad/api/main.py b/tad/api/main.py index 6f604c7e..5b843768 100644 --- a/tad/api/main.py +++ b/tad/api/main.py @@ -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"]) diff --git a/tad/api/routes/pages.py b/tad/api/routes/pages.py new file mode 100644 index 00000000..73c31fe3 --- /dev/null +++ b/tad/api/routes/pages.py @@ -0,0 +1,21 @@ +from fastapi import APIRouter, Request +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates + +from tad.services.statuses import StatusesService +from tad.services.tasks import TasksService + +tasks_service = TasksService() +statuses_service = StatusesService() +router = APIRouter() +templates = Jinja2Templates(directory="tad/site/templates") + + +@router.get("/", response_class=HTMLResponse) +async def default_layout(request: Request): + context = { + "page_title": "This is the page title", + "tasks_service": tasks_service, + "statuses_service": statuses_service, + } + return templates.TemplateResponse(request=request, name="default_layout.jinja", context=context) diff --git a/tad/api/routes/tasks.py b/tad/api/routes/tasks.py new file mode 100644 index 00000000..739fb3c1 --- /dev/null +++ b/tad/api/routes/tasks.py @@ -0,0 +1,26 @@ +from fastapi import APIRouter, Request +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates + +from tad.services.tasks import TasksService + +router = APIRouter() + +tasks_service = TasksService() +templates = Jinja2Templates(directory="tad/site/templates") + + +@router.get("/") +async def test(): + return [{"username": "Rick"}, {"username": "Morty"}] + + +@router.post("/move", response_class=HTMLResponse) +async def move_task(request: Request): + json = await request.json() + + print(json) + task = tasks_service.move_task( + int(json["taskId"]), int(json["statusId"]), json["previousSiblingId"], json["nextSiblingId"] + ) + return templates.TemplateResponse(request=request, name="task.jinja", context={"task": task}) diff --git a/tad/core/config.py b/tad/core/config.py index d2e69590..de95126a 100644 --- a/tad/core/config.py +++ b/tad/core/config.py @@ -15,6 +15,11 @@ # Self type is not available in Python 3.10 so create our own with TypeVar SelfSettings = TypeVar("SelfSettings", bound="Settings") +logger = logging.getLogger(__name__) + +print("My name is " + __name__) +logger.info("Hallo ik ben een logOOOOOOOOger") + class Settings(BaseSettings): # todo(berry): investigate yaml, toml or json file support for SettingsConfigDict @@ -42,7 +47,7 @@ def server_host(self) -> str: PROJECT_NAME: str = "TAD" PROJECT_DESCRIPTION: str = "Transparency of Algorithmic Decision making" - STATIC_DIR: str = "tad/site/static" + STATIC_DIR: str = "tad/site/static/" TEMPLATE_DIR: str = "tad/templates" # todo(berry): create submodel for database settings diff --git a/tad/core/singleton.py b/tad/core/singleton.py new file mode 100644 index 00000000..845ba727 --- /dev/null +++ b/tad/core/singleton.py @@ -0,0 +1,16 @@ +from typing import ClassVar + + +class Singleton(type): + """The Singleton metaclass can be used to mark classes as singleton. + Based on https://stackoverflow.com/questions/6760685/what-is-the-best-way-of-implementing-singleton-in-python + + Usage: class Classname(metaclass=Singleton): + """ + + _instances = ClassVar[{}] + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super().__call__(*args, **kwargs) + return cls._instances[cls] diff --git a/tad/main.py b/tad/main.py index 76b18892..681a88d4 100644 --- a/tad/main.py +++ b/tad/main.py @@ -18,10 +18,11 @@ validation_exception_handler as tad_validation_exception_handler, ) from tad.core.log import configure_logging -from tad.middleware.route_logging import RequestLoggingMiddleware +from tad.repositories.tasks import TasksRepository from tad.utils.mask import Mask -from .routers import pages, tasks +from .middleware.route_logging import RequestLoggingMiddleware +from .repositories.statuses import StatusesRepository configure_logging(settings.LOGGING_LEVEL, settings.LOGGING_CONFIG) @@ -56,8 +57,6 @@ async def lifespan(app: FastAPI): app.add_middleware(RequestLoggingMiddleware) app.mount("/static", StaticFiles(directory=settings.STATIC_DIR), name="static") -app.include_router(tasks.router) -app.include_router(pages.router) @app.exception_handler(StarletteHTTPException) @@ -71,3 +70,8 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE app.include_router(api_router) + +tasks_repository = TasksRepository() +statuses_repository = StatusesRepository() + +logger.info("Hallo ik ben een logger") diff --git a/tad/models/task.py b/tad/models/task.py index 12eef9c2..dc64ef08 100644 --- a/tad/models/task.py +++ b/tad/models/task.py @@ -6,5 +6,6 @@ class Task(SQLModel, table=True): title: str description: str sort_order: float - status_id: int + status_id: int | None = Field(default=None, foreign_key="status.id") user_id: int | None = Field(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 diff --git a/tad/routers/__init__.py b/tad/repositories/__init__.py similarity index 100% rename from tad/routers/__init__.py rename to tad/repositories/__init__.py diff --git a/tad/repositories/statuses.py b/tad/repositories/statuses.py new file mode 100644 index 00000000..25b4a380 --- /dev/null +++ b/tad/repositories/statuses.py @@ -0,0 +1,45 @@ +import logging +from collections.abc import Sequence + +from sqlmodel import Session, select + +from tad.core.db import engine +from tad.core.singleton import Singleton +from tad.models import Status + +logger = logging.getLogger(__name__) + + +class StatusesRepository(metaclass=Singleton): + # TODO find out how to reuse Session + + def __init__(self): + logger.info("Hello world from statuses repo") + statuses = self.find_all() + if len(statuses) == 0: + self.__add_test_statuses() + + def __add_test_statuses(self): + with Session(engine) as session: + session.add(Status(id=1, name="todo", sort_order=1)) + session.add(Status(id=2, name="in_progress", sort_order=2)) + session.add(Status(id=3, name="review", sort_order=3)) + session.add(Status(id=4, name="done", sort_order=4)) + session.commit() + + def find_all(self) -> Sequence[Status]: + with Session(engine) as session: + statement = select(Status) + return session.exec(statement).all() + + def save(self, status) -> Status: + with Session(engine) as session: + session.add(status) + session.commit() + session.refresh(status) + return status + + def find_by_id(self, status_id) -> Status: + with Session(engine) as session: + statement = select(Status).where(Status.id == status_id) + return session.exec(statement).one() diff --git a/tad/repositories/tasks.py b/tad/repositories/tasks.py new file mode 100644 index 00000000..3901682c --- /dev/null +++ b/tad/repositories/tasks.py @@ -0,0 +1,51 @@ +from collections.abc import Sequence + +from sqlmodel import Session, select + +from tad.core.db import engine +from tad.core.singleton import Singleton +from tad.models import Task + + +class TasksRepository(metaclass=Singleton): + def __init__(self): + tasks = self.find_all() + if len(tasks) == 0: + self.__add_test_tasks() + + def __add_test_tasks(self): + with Session(engine) as session: + session.add( + Task( + status_id=1, + title="IAMA", + description="Impact Assessment Mensenrechten en Algoritmes", + sort_order=10, + ) + ) + session.add(Task(status_id=1, title="SHAP", description="SHAP", sort_order=20)) + session.add(Task(status_id=1, title="This is title 3", description="This is description 3", sort_order=30)) + session.commit() + + def find_all(self) -> Sequence[Task]: + """Returns all the tasks from the repository.""" + with Session(engine) as session: + statement = select(Task) + return session.exec(statement).all() + + def find_by_status_id(self, status_id) -> Sequence[Task]: + with Session(engine) as session: + statement = select(Task).where(Task.status_id == status_id).order_by(Task.sort_order) + return session.exec(statement).all() + + def save(self, task) -> Task: + with Session(engine) as session: + session.add(task) + session.commit() + session.refresh(task) + return task + + def find_by_id(self, task_id) -> Task: + with Session(engine) as session: + statement = select(Task).where(Task.id == task_id) + return session.exec(statement).one() diff --git a/tad/routers/pages.py b/tad/routers/pages.py deleted file mode 100644 index 8c429cc8..00000000 --- a/tad/routers/pages.py +++ /dev/null @@ -1,20 +0,0 @@ -from fastapi import APIRouter -from fastapi.responses import HTMLResponse -from jinja2 import Environment, FileSystemLoader - -from tad.services.tasks_service import TasksService - -env = Environment(loader=FileSystemLoader("tad/site/templates"), autoescape=True) -tasks_service = TasksService() - -router = APIRouter( - prefix="/pages", - tags=["pages"], -) - - -@router.get("/", response_class=HTMLResponse) -async def default_layout(): - index_template = env.get_template("default_layout.jinja") - template_data = {"page_title": "This is the page title", "tasks_service": tasks_service} - return index_template.render(template_data) diff --git a/tad/routers/tasks.py b/tad/routers/tasks.py deleted file mode 100644 index 326153c2..00000000 --- a/tad/routers/tasks.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import Annotated - -from fastapi import APIRouter, Form, Request -from fastapi.responses import HTMLResponse -from fastapi.templating import Jinja2Templates - -from tad.services.tasks_service import TasksService - -router = APIRouter( - prefix="/tasks", - tags=["tasks"], -) - -tasks_service = TasksService() -templates = Jinja2Templates(directory="tad/site/templates") - - -@router.get("/") -async def test(): - return [{"username": "Rick"}, {"username": "Morty"}] - - -@router.post("/move", response_class=HTMLResponse) -async def move_task(request: Request): - json = await request.json() - print(json) - task = tasks_service.move_task( - int(json["taskId"]), int(json["statusId"]), json["previousSiblingId"], json["nextSiblingId"] - ) - return templates.TemplateResponse(request=request, name="task.jinja", context={"task": task}) - - -# TODO this is an ugly work-around, we need a JSON object instead -@router.post("/move-form", response_class=HTMLResponse) -async def move_task_form( - request: Request, - taskId: Annotated[int, Form()], - statusId: Annotated[int, Form()], - previousSiblingId: int | None = Form(None), - nextSiblingId: int | None = Form(None), -): - task = tasks_service.move_task(taskId, statusId, previousSiblingId, nextSiblingId) - return templates.TemplateResponse(request=request, name="task.jinja", context={"task": task}) diff --git a/tad/services/statuses.py b/tad/services/statuses.py new file mode 100644 index 00000000..b9ce4229 --- /dev/null +++ b/tad/services/statuses.py @@ -0,0 +1,20 @@ +import logging + +from tad.core.singleton import Singleton +from tad.repositories.statuses import StatusesRepository + +logger = logging.getLogger(__name__) + + +class StatusesService(metaclass=Singleton): + __statuses_repository = StatusesRepository() + + def __init__(self): + # TODO find out why logging is not visible + print("I am created, hello world") + + def get_status(self, status_id): + return self.__statuses_repository.find_by_id(status_id) + + def get_statuses(self) -> []: + return self.__statuses_repository.find_all() diff --git a/tad/services/tasks.py b/tad/services/tasks.py new file mode 100644 index 00000000..9292d268 --- /dev/null +++ b/tad/services/tasks.py @@ -0,0 +1,58 @@ +import logging + +from tad.core.singleton import Singleton +from tad.models.task import Task +from tad.models.user import User +from tad.repositories.tasks import TasksRepository +from tad.services.statuses import StatusesService + +logger = logging.getLogger(__name__) + + +class TasksService(metaclass=Singleton): + __tasks_repository = TasksRepository() + __statuses_service = StatusesService() + + def __init__(self): + pass + + def get_tasks(self, status_id): + return self.__tasks_repository.find_by_status_id(status_id) + + def assign_task(self, task: Task, user: User): + task.user_id = user.id + self.__tasks_repository.save(task) + + def move_task(self, task_id, status_id, previous_sibling_id, next_sibling_id) -> Task: + status = self.__statuses_service.get_status(status_id) + task = self.__tasks_repository.find_by_id(task_id) + + if status.name == "done": + # TODO implement logic for done + logging.warning("Task is done, we need to update a system card") + + # assign the task to the current user + if status.name == "in_progress": + task.user_id = 1 + + # update the status for the task (this may not be needed if the status has not changed) + task.status_id = status_id + + # update order position of the card + if not previous_sibling_id and not next_sibling_id: + task.sort_order = 10 + elif previous_sibling_id and next_sibling_id: + previous_task = self.__tasks_repository.find_by_id(int(previous_sibling_id)) + next_task = self.__tasks_repository.find_by_id(int(next_sibling_id)) + new_sort_order = previous_task.sort_order + ((next_task.sort_order - previous_task.sort_order) / 2) + task.sort_order = new_sort_order + elif previous_sibling_id and not next_sibling_id: + previous_task = self.__tasks_repository.find_by_id(int(previous_sibling_id)) + task.sort_order = previous_task.sort_order + 10 + elif not previous_sibling_id and next_sibling_id: + next_task = self.__tasks_repository.find_by_id(int(next_sibling_id)) + task.sort_order = next_task.sort_order / 2 + + task = self.__tasks_repository.save(task) + + return task diff --git a/tad/services/tasks_service.py b/tad/services/tasks_service.py deleted file mode 100644 index a495e495..00000000 --- a/tad/services/tasks_service.py +++ /dev/null @@ -1,96 +0,0 @@ -from sqlmodel import Session - -from tad.core.db import engine -from tad.models.status import Status -from tad.models.task import Task -from tad.models.user import User - - -class TasksService: - _self = None - _statuses = list[Status] - _tasks = list[Task] - - # make sure this is a singleton class, this may not be needed? - - def __init__(self): - # this is dummy data to get started, this should be retrieved from the database - # TODO all status and task retrieval should be database calls - self._statuses = [] - self._statuses.append(Status(id=1, name="todo", sort_order=1)) - self._statuses.append(Status(id=2, name="in_progress", sort_order=2)) - self._statuses.append(Status(id=3, name="review", sort_order=3)) - self._statuses.append(Status(id=4, name="done", sort_order=4)) - - self._tasks = [] - self._tasks.append( - Task( - id=1, - status_id=1, - title="IAMA", - description="Impact Assessment Mensenrechten en Algoritmes", - sort_order=10, - ) - ) - self._tasks.append(Task(id=2, status_id=1, title="SHAP", description="SHAP", sort_order=20)) - self._tasks.append( - Task(id=3, status_id=1, title="This is title 3", description="This is description 3", sort_order=30) - ) - - def _get_task_by_id(self, task_id: int) -> Task: - return next(task for task in self._tasks if task.id == task_id) - - def _get_status_by_id(self, status_id: int) -> Status: - print(status_id) - return next(status for status in self._statuses if status.id == status_id) - - def get_statuses(self) -> []: - return self._statuses - - def get_tasks(self, status_id): - # TODO lines below probably can be simplified / combined - res = [val for val in self._tasks if val.status_id == status_id] - sorted_res = sorted(res, key=lambda sort_task: sort_task.sort_order) # sort by age - return sorted_res - - def assign_task(self, task: Task, user: User): - task.user_id = user.id - # TODO persist to database and / or decided when to persist (combined calls) - - def get_status(self, status_id) -> Status: - return self._get_status_by_id(status_id) - - def get_task(self, task_id) -> Task: - return self._get_task_by_id(task_id) - - def move_task(self, task_id, status_id, previous_sibling_id, next_sibling_id) -> Task: - status = self.get_status(status_id) - task = self.get_task(task_id) - - # assign the task to the current user - if status.name == "in_progress": - task.user_id = 1 - - # update the status for the task (this may not be needed if the status has not changed) - task.status_id = status_id - - # update order position of the card - if not previous_sibling_id and not next_sibling_id: - task.sort_order = 10 - elif previous_sibling_id and next_sibling_id: - previous_task = self.get_task(int(previous_sibling_id)) - next_task = self.get_task(int(next_sibling_id)) - new_sort_order = previous_task.sort_order + ((next_task.sort_order - previous_task.sort_order) / 2) - task.sort_order = new_sort_order - elif previous_sibling_id and not next_sibling_id: - previous_task = self.get_task(int(previous_sibling_id)) - task.sort_order = previous_task.sort_order + 10 - elif not previous_sibling_id and next_sibling_id: - next_task = self.get_task(int(next_sibling_id)) - task.sort_order = next_task.sort_order / 2 - - with Session(engine) as session: - session.add(task) - session.commit() - - return task diff --git a/tad/static/vendor/htmx/1.9.12.min.js b/tad/site/static/js/vendor/1.9.12.min.js similarity index 100% rename from tad/static/vendor/htmx/1.9.12.min.js rename to tad/site/static/js/vendor/1.9.12.min.js diff --git a/tad/site/static/js/htmx.min.js b/tad/site/static/js/vendor/htmx.min.js similarity index 100% rename from tad/site/static/js/htmx.min.js rename to tad/site/static/js/vendor/htmx.min.js diff --git a/tad/site/static/js/hyperscript.min.js b/tad/site/static/js/vendor/hyperscript.min.js similarity index 100% rename from tad/site/static/js/hyperscript.min.js rename to tad/site/static/js/vendor/hyperscript.min.js diff --git a/tad/site/static/js/sortable.js b/tad/site/static/js/vendor/sortable.js similarity index 100% rename from tad/site/static/js/sortable.js rename to tad/site/static/js/vendor/sortable.js diff --git a/tad/site/templates/default_layout.jinja b/tad/site/templates/default_layout.jinja index e4f8bb49..479744cb 100644 --- a/tad/site/templates/default_layout.jinja +++ b/tad/site/templates/default_layout.jinja @@ -18,10 +18,10 @@