freelancer/flowboard
Boards starter for the Dodil Boards Challenge. FastAPI + DataK3 + appid + Models + MCP.
A remix template · 0 remixes · Remix freelancer/flowboard
app/boards/application/use_cases.py
"""Application use cases shared by the web API and MCP tools."""
import uuid
from dataclasses import asdict
from datetime import date
from typing import Optional
from boards.application.ports import Assistant, BoardRepository
from boards.domain.model import (
Board,
Card,
Forbidden,
Invalid,
Person,
Role,
)
class NotFound(Exception):
"""The requested resource does not exist or is not visible."""
def _card(card: Card) -> dict:
"""Convert a domain card to a JSON-friendly dictionary."""
data = asdict(card)
data["due_date"] = (
card.due_date.isoformat() if card.due_date else None
)
return data
class Boards:
def __init__(
self,
repo: BoardRepository,
assistant: Optional[Assistant] = None,
):
self.repo = repo
self.assistant = assistant
def _board(self, board_id: str, person: Person) -> Board:
"""Return a board only when the person can view it."""
board = self.repo.get(board_id)
if board is None:
raise NotFound("no such board")
try:
board.require_view(person)
except Forbidden as exc:
raise NotFound("no such board") from exc
return board
def _find_card(self, card_id: str, person: Person) -> tuple[Board, Card]:
"""Find a card and verify access to its board."""
card = self.repo.get_card(card_id)
if card is None:
raise NotFound("no such card")
board = self._board(card.board_id, person)
return board, card
def create_board(self, person: Person, title: str) -> dict:
"""Create a board owned by the authenticated person."""
if not title.strip():
raise Invalid("a board needs a title")
board = Board(
id=uuid.uuid4().hex[:12],
title=title.strip(),
owner_email=person.email,
)
self.repo.save_board(board)
self.repo.log(
board.id,
None,
person.email,
f"created board {board.title!r}",
)
return {"id": board.id, "title": board.title}
def my_boards(self, person: Person) -> list:
"""List boards visible to the authenticated person."""
return [
{
"id": board.id,
"title": board.title,
"role": board.role_of(person).value,
}
for board in self.repo.boards_for(person.email)
if board.role_of(person) is not None
]
def show_board(self, person: Person, board_id: str) -> dict:
"""Return a board and its cards after checking visibility."""
board = self._board(board_id, person)
cards = [
_card(card)
for card in self.repo.cards_on(board.id)
]
return {
"id": board.id,
"title": board.title,
"role": board.role_of(person).value,
"cards": cards,
}
def add_member(
self,
person: Person,
board_id: str,
email: str,
role: str,
) -> dict:
"""Add or update a member; only the owner may manage membership."""
board = self._board(board_id, person)
try:
selected_role = Role(role)
except ValueError as exc:
raise Invalid(
"role must be 'member' or 'viewer'"
) from exc
normalized_email = email.strip().lower()
if not normalized_email or "@" not in normalized_email:
raise Invalid("enter a valid email address")
board.add_member(person, normalized_email, selected_role)
self.repo.save_board(board)
self.repo.log(
board.id,
None,
person.email,
f"added {normalized_email} as {selected_role.value}",
)
return {
"board_id": board.id,
"email": normalized_email,
"role": selected_role.value,
}
def add_card(
self,
person: Person,
board_id: str,
title: str,
list_name: str = "To do",
) -> dict:
"""Create a card after checking visibility and edit access."""
board = self._board(board_id, person)
board.require_edit(person)
if not title.strip():
raise Invalid("a card needs a title")
card = Card(
id=uuid.uuid4().hex[:12],
board_id=board.id,
title=title.strip(),
)
card.move_to(list_name)
self.repo.save_card(card)
self.repo.log(
board.id,
card.id,
person.email,
f"added card {card.title!r}",
)
return _card(card)
def move_card(
self,
person: Person,
card_id: str,
list_name: str,
) -> dict:
"""Move a card between allowed lists."""
board, card = self._find_card(card_id, person)
board.require_edit(person)
previous_list = card.list_name
card.move_to(list_name)
if card.list_name != previous_list:
self.repo.save_card(card)
self.repo.log(
board.id,
card.id,
person.email,
f"moved from {previous_list} to {card.list_name}",
)
return _card(card)
def update_card(
self,
person: Person,
card_id: str,
*,
title: Optional[str] = None,
description: Optional[str] = None,
assignee: Optional[str] = None,
due_date: Optional[str] = None,
labels: Optional[list[str]] = None,
clear_assignee: bool = False,
clear_due_date: bool = False,
) -> dict:
"""Update supplied fields and log any actual changes."""
board, card = self._find_card(card_id, person)
board.require_edit(person)
parsed_due_date = None
if due_date is not None:
if due_date == "":
clear_due_date = True
else:
try:
parsed_due_date = date.fromisoformat(due_date)
except ValueError as exc:
raise Invalid(
"due_date must use YYYY-MM-DD format"
) from exc
if assignee == "":
clear_assignee = True
old = _card(card)
card.update_details(
title=title,
description=description,
assignee=assignee,
due_date=parsed_due_date,
labels=labels,
clear_assignee=clear_assignee,
clear_due_date=clear_due_date,
)
new = _card(card)
changed_fields = [
field_name
for field_name in (
"title",
"description",
"assignee",
"due_date",
"labels",
)
if old[field_name] != new[field_name]
]
if changed_fields:
self.repo.save_card(card)
self.repo.log(
board.id,
card.id,
person.email,
"updated card fields: " + ", ".join(changed_fields),
)
return new
def activity_history(
self,
person: Person,
board_id: str,
limit: int = 100,
) -> dict:
"""Return recent board activity to a person who can view the board."""
board = self._board(board_id, person)
try:
requested_limit = int(limit)
except (TypeError, ValueError) as exc:
raise Invalid("activity limit must be an integer") from exc
if requested_limit < 1 or requested_limit > 500:
raise Invalid("activity limit must be between 1 and 500")
events = self.repo.activity_for(board.id, requested_limit)
return {
"board_id": board.id,
"board_title": board.title,
"activity": events,
}
def suggest_checklist(
self,
person: Person,
card_id: str,
) -> dict:
"""Suggest an AI-generated checklist without modifying the card."""
_, card = self._find_card(card_id, person)
if self.assistant is None:
raise Invalid("no AI assistant configured")
return {
"card_id": card.id,
"suggested_checklist": self.assistant.checklist(card.title),
}