freelancer/flowboard
Boards starter for the Dodil Boards Challenge. FastAPI + DataK3 + appid + Models + MCP.
A remix template · 0 remixes · Remix freelancer/flowboard
app/boards/domain/model.py
"""The board's language and rules. No database or HTTP dependencies."""
from dataclasses import dataclass, field
from datetime import date
from enum import Enum
from typing import Optional
LISTS = ("To do", "Doing", "Done")
class Role(str, Enum):
OWNER = "owner"
MEMBER = "member"
VIEWER = "viewer"
class Forbidden(Exception):
"""The person may not perform this action."""
class Invalid(Exception):
"""The request violates a board rule."""
@dataclass(frozen=True)
class Person:
"""Identity comes from Dodil authentication, never the request body."""
email: str
@dataclass
class Card:
id: str
board_id: str
title: str
list_name: str = LISTS[0]
assignee: Optional[str] = None
due_date: Optional[date] = None
description: str = ""
labels: list[str] = field(default_factory=list)
def move_to(self, list_name: str) -> None:
if list_name not in LISTS:
raise Invalid(
f"unknown list {list_name!r}; lists are {', '.join(LISTS)}"
)
self.list_name = list_name
def update_details(
self,
*,
title: Optional[str] = None,
description: Optional[str] = None,
assignee: Optional[str] = None,
due_date: Optional[date] = None,
labels: Optional[list[str]] = None,
clear_assignee: bool = False,
clear_due_date: bool = False,
) -> None:
"""Update card details and explicitly support clearing optional fields."""
if title is not None:
cleaned_title = title.strip()
if not cleaned_title:
raise Invalid("a card needs a title")
self.title = cleaned_title
if description is not None:
self.description = description.strip()
if clear_assignee:
self.assignee = None
elif assignee is not None:
self.assignee = assignee.strip().lower() or None
if clear_due_date:
self.due_date = None
elif due_date is not None:
self.due_date = due_date
if labels is not None:
self.labels = list(
dict.fromkeys(
label.strip()
for label in labels
if label.strip()
)
)
@dataclass
class Board:
id: str
title: str
owner_email: str
members: dict = field(default_factory=dict)
def role_of(self, person: Person) -> Optional[Role]:
if person.email == self.owner_email:
return Role.OWNER
return self.members.get(person.email)
def require_view(self, person: Person) -> None:
if self.role_of(person) is None:
raise Forbidden("you are not on this board")
def require_edit(self, person: Person) -> None:
role = self.role_of(person)
if role is None:
raise Forbidden("you are not on this board")
if role is Role.VIEWER:
raise Forbidden("viewers can't change the board")
def add_member(self, by: Person, email: str, role: Role) -> None:
if self.role_of(by) is not Role.OWNER:
raise Forbidden("only the owner manages members")
if role is Role.OWNER:
raise Invalid("a board has one owner")
normalized_email = email.strip().lower()
if not normalized_email:
raise Invalid("a member needs an email")
if normalized_email == self.owner_email:
raise Invalid("the owner already owns this board")
self.members[normalized_email] = role