Files
blog.pyaqa.ru/app/presentation/web/routes.py
Sergey Vanyushkin e2802d83f2 feat(ui): add web UI with Jinja2 templates and Gitea themes
- Add Jinja2 templates with data-testid attributes for testing
- Create light/dark themes based on Gitea color scheme
- Add theme switching with localStorage persistence
- Create base CSS, components, and layout styles
- Add mock web routes for UI demonstration
- Register web router and static files in main.py
- Add data-testid requirements to AGENTS.md
- Install jinja2 dependency
2026-05-02 14:45:51 +03:00

328 lines
9.4 KiB
Python

"""Web UI routes for blog application.
This module provides HTML endpoints for the blog web interface.
Currently uses mock data for demonstration purposes.
Integration with use cases will be added in future iterations.
"""
from datetime import datetime
from uuid import uuid4
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
router = APIRouter(prefix="/web", tags=["web"])
templates = Jinja2Templates(directory="app/presentation/templates")
class MockPost:
"""Mock post object for UI demonstration.
This class simulates a Post entity for template rendering
before integration with actual use cases.
Attributes:
id: Unique identifier for the post.
title: Post title value object.
content: Post content value object.
slug: URL-friendly slug.
author_id: Identifier of the post author.
published: Publication status flag.
tags: List of tags associated with the post.
created_at: Timestamp when the post was created.
updated_at: Timestamp when the post was last updated.
"""
def __init__(
self,
id: str,
title: str,
content: str,
slug: str,
author_id: str,
published: bool,
tags: list[str],
created_at: datetime | None = None,
) -> None:
"""Initialize mock post with provided attributes.
Args:
id: Unique identifier for the post.
title: Post title string.
content: Post content string.
slug: URL-friendly slug string.
author_id: Author identifier string.
published: Whether the post is published.
tags: List of tag strings.
created_at: Optional creation timestamp, defaults to now.
"""
self.id = id
self.title = MockValueObject(title)
self.content = MockValueObject(content)
self.slug = MockValueObject(slug)
self.author_id = author_id
self.published = published
self.tags = tags
self.created_at = created_at or datetime.now()
self.updated_at = self.created_at
class MockValueObject:
"""Mock value object for simulating domain value objects.
Wraps a raw value to simulate the interface of domain
value objects like Title, Content, and Slug.
Attributes:
value: The wrapped string value.
"""
def __init__(self, value: str) -> None:
"""Initialize with a string value.
Args:
value: The string value to wrap.
"""
self.value = value
MOCK_POSTS = [
MockPost(
id=str(uuid4()),
title="Getting Started with FastAPI",
content="FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. It is designed to be easy to use while providing high performance.",
slug="getting-started-with-fastapi",
author_id="john_doe",
published=True,
tags=["python", "fastapi", "tutorial"],
created_at=datetime(2026, 1, 15, 10, 30),
),
MockPost(
id=str(uuid4()),
title="Understanding DDD Architecture",
content="Domain-Driven Design (DDD) is an approach to software development that centers the development on programming a domain model that has a rich understanding of the processes and rules of a domain. The term was coined by Eric Evans in his book of the same title.",
slug="understanding-ddd-architecture",
author_id="jane_smith",
published=True,
tags=["ddd", "architecture", "software-design"],
created_at=datetime(2026, 1, 14, 14, 45),
),
MockPost(
id=str(uuid4()),
title="Draft Post Example",
content="This is a draft post that hasn't been published yet. It demonstrates how unpublished posts appear in the UI.",
slug="draft-post-example",
author_id="john_doe",
published=False,
tags=["draft"],
created_at=datetime(2026, 1, 13, 9, 0),
),
]
@router.get("/", response_class=HTMLResponse)
async def home(request: Request) -> HTMLResponse:
"""Render the home page with list of posts.
Args:
request: The HTTP request object for template context.
Returns:
HTMLResponse with rendered posts list template.
"""
return templates.TemplateResponse(
request,
"pages/index.html",
{
"posts": MOCK_POSTS,
"active_page": "home",
"current_page": 1,
"has_prev": False,
"has_next": False,
},
)
@router.get("/posts", response_class=HTMLResponse)
async def list_posts(request: Request) -> HTMLResponse:
"""Render the posts listing page.
Args:
request: The HTTP request object for template context.
Returns:
HTMLResponse with rendered posts list template.
"""
return templates.TemplateResponse(
request,
"pages/index.html",
{
"posts": MOCK_POSTS,
"active_page": "posts",
"current_page": 1,
"has_prev": False,
"has_next": True,
},
)
@router.get("/posts/new", response_class=HTMLResponse)
async def new_post_form(request: Request) -> HTMLResponse:
"""Render the new post creation form.
Args:
request: The HTTP request object for template context.
Returns:
HTMLResponse with rendered post form template.
"""
return templates.TemplateResponse(
request,
"pages/post_form.html",
{
"is_edit": False,
"post": None,
"active_page": "posts",
},
)
@router.post("/posts/new", response_class=HTMLResponse)
async def create_post(request: Request) -> HTMLResponse:
"""Handle new post creation form submission.
Args:
request: The HTTP request object containing form data.
Returns:
HTMLResponse redirecting to the home page.
"""
return templates.TemplateResponse(
request,
"pages/index.html",
{
"posts": MOCK_POSTS,
"active_page": "home",
"current_page": 1,
"has_prev": False,
"has_next": False,
},
)
@router.get("/posts/{post_id}", response_class=HTMLResponse)
async def post_detail(request: Request, post_id: str) -> HTMLResponse:
"""Render a single post detail page.
Args:
request: The HTTP request object for template context.
post_id: The unique identifier of the post to display.
Returns:
HTMLResponse with rendered post detail template.
"""
post = next((p for p in MOCK_POSTS if str(p.id) == post_id), MOCK_POSTS[0])
return templates.TemplateResponse(
request,
"pages/post_detail.html",
{
"post": post,
"active_page": "posts",
},
)
@router.get("/posts/{post_id}/edit", response_class=HTMLResponse)
async def edit_post_form(request: Request, post_id: str) -> HTMLResponse:
"""Render the post edit form.
Args:
request: The HTTP request object for template context.
post_id: The unique identifier of the post to edit.
Returns:
HTMLResponse with rendered post form template.
"""
post = next((p for p in MOCK_POSTS if str(p.id) == post_id), MOCK_POSTS[0])
return templates.TemplateResponse(
request,
"pages/post_form.html",
{
"is_edit": True,
"post": post,
"active_page": "posts",
},
)
@router.post("/posts/{post_id}/edit", response_class=HTMLResponse)
async def update_post(request: Request, post_id: str) -> HTMLResponse:
"""Handle post update form submission.
Args:
request: The HTTP request object containing form data.
post_id: The unique identifier of the post to update.
Returns:
HTMLResponse with rendered post detail template.
"""
post = next((p for p in MOCK_POSTS if str(p.id) == post_id), MOCK_POSTS[0])
return templates.TemplateResponse(
request,
"pages/post_detail.html",
{
"post": post,
"active_page": "posts",
},
)
@router.post("/posts/{post_id}/delete", response_class=HTMLResponse)
async def delete_post(request: Request, post_id: str) -> HTMLResponse:
"""Handle post deletion.
Args:
request: The HTTP request object.
post_id: The unique identifier of the post to delete.
Returns:
HTMLResponse redirecting to the home page.
"""
return templates.TemplateResponse(
request,
"pages/index.html",
{
"posts": MOCK_POSTS,
"active_page": "home",
"current_page": 1,
"has_prev": False,
"has_next": False,
},
)
@router.get("/about", response_class=HTMLResponse)
async def about(request: Request) -> HTMLResponse:
"""Render the about page.
Args:
request: The HTTP request object for template context.
Returns:
HTMLResponse with rendered about page template.
"""
return HTMLResponse(
content="""
<!DOCTYPE html>
<html>
<head><title>About - Blog</title></head>
<body>
<h1>About</h1>
<p>A modern blog built with FastAPI and DDD architecture.</p>
<a href="/web/">Back to home</a>
</body>
</html>
"""
)