iT邦幫忙

2024 iThome 鐵人賽

DAY 29
0
Python

眾裏尋它:Python表格利器Great Tables系列 第 29

[Day29] - 如何與FastHTML整合 - 動態表格

  • 分享至 

  • xImage
  •  

今天我們繼續來了解呈現如何在FastHTML app中呈現動態表格及使用Playwright進行互動測試。

動態表格

您可以於FastHTML Gallery網站,實際測試今日的成果。

步驟1:建立fast_app instance

與靜態表格一樣,建立一個名為appfast_app instance及一個rt object。

# https://github.com/jrycw/ft-gt-demo/blob/master/app.py
from functools import cache

import polars as pl
from fasthtml.common import (H1, H2, Card, Div, Form, Grid, Input, Main, 
                             Title, fast_app)
from great_tables import GT, html
from great_tables.data import sza

from ft_gt import gt2fasthtml

app, rt = fast_app()

步驟2:建立回傳Polars DataFrame的get_sza_pivot()函數

建立get_sza_pivot()函數,其會回傳一個Polars DataFrame。由於在此app中,DataFrame並不會變動,所以這邊可以使用@cache裝飾在get_sza_pivot()之上。

# https://github.com/jrycw/ft-gt-demo/blob/master/app.py
@cache
def get_sza_pivot():
    return (
        pl.from_pandas(sza)
        .filter((pl.col("latitude") == "20") & (pl.col("tst") <= "1200"))
        .select(pl.col("*").exclude("latitude"))
        .drop_nulls()
        .pivot(values="sza", index="month", on="tst", sort_columns=True)
    )

請留意,因為今天的範例只會進行與gt表格的互動,而不牽涉到DataFrame的整理,所以我將與DataFrame整理相關的程式合併於此函數。

步驟3:建立回傳FastHTML component的get_gtbl()函數

建立get_gtbl()函數,其接收color1color2兩個變數,用以設定表格的背景顏色,最後則會回傳GT instance。

# https://github.com/jrycw/ft-gt-demo/blob/master/app.py
@gt2fasthtml(id="gt")
def get_gtbl(color1: str = "#663399", color2: str = "#FFA500"):
    return (
        GT(get_sza_pivot(), rowname_col="month")
        .data_color(
            domain=[90, 0],
            palette=[color1, "white", color2],
            na_color="white",
        )
        .tab_header(
            title="Solar Zenith Angles from 05:30 to 12:00",
            subtitle=html("Average monthly values at latitude of 20&deg;N."),
        )
        .sub_missing(missing_text="")
    )

請留意此處我們將@gt2fasthtml(id="gt")裝飾於get_gtbl()上,這麼一來這段程式碼可以這麼理解:

  • get_gtbl()變成了一個回傳id為「"gt"」的div tag。
  • div tag又包裹了一個NotStr FastHTML component。
  • NotStr FastHTML component才真正包裹了HTML格式的gt表格。

步驟4:建立接收form的post()函數

這裡我們使用@app.post("/submit")定義post()為使用者以HTTP POST訪問「"/submit"」時使用的函數。相比於使用@rt("/submit"),或許您會更習慣這種Flask或是FastAPI的用法。

# https://github.com/jrycw/ft-gt-demo/blob/master/app.py
@app.post("/submit")
def post(d: dict):
    return get_gtbl(**d)

post()所接收的d是一個字典型別,當我們在網頁上改變稍後會建立的顏色選擇器顏色時,其會包含所選的color1color2的值,像是{'color1': '#663399', 'color2': '#ffa500'};而post()則會回傳get_gtbl(**d)的結果。

也就是說,這個endpoint將根據所傳入的d來生成新的gt表格,並會自動將其包裹於適當的HTML tag或是FastHTML component中回傳。

步驟5:建立回傳表格的homepage()函數

使用@rt("/")來定義get(),作為使用者以HTTP GET訪問「"/"」時所使用的函數,其將會直接回傳各種HTML的tag或其內建的component。

# https://github.com/jrycw/ft-gt-demo/blob/master/app.py
@app.get("/")
def homepage():
    return (
        Title("FastHTML-GT Website"),
        H1("Great Tables shown in FastHTML", style="text-align:center"),
        Main(
            Form(
                hx_post="/submit",
                hx_target="#gt",
                hx_trigger="input",
                hx_swap="outerHTML",
            )(
                Grid(
                    Div(),
                    Card(
                        H2("Color1"),
                        Input(type="color", id="color1", value="#663399"),
                    ),
                    Card(
                        H2("Color2"),
                        Input(type="color", id="color2", value="#FFA500"),
                    ),
                    Div(),
                )
            ),
            get_gtbl(),
            cls="container",
        ),
    )

其中TitleH1部份與靜態表格相同,不再重複解釋。

這裡值得注意的是Main,其中get_gtbl()是一個id為「"gt"」的div tag,而class attribute設為container,這兩個是比較好理解的部份。

接下來說說比較複雜的Form,其接受許多HTMX參數的設定:

  • hx_post對應HTMX中的hx-post,其功用是設定當form進行HTTP POST時所訪問的URL,即「"/submit"」。
  • hx_target對應HTMX中的hx-target,其功用是指定form的回傳值所替換的對象,即id為「"gt"」的tag,也就是get_gtbl()
  • hx_trigger對應HTMX中的hx-trigger,其功用是指定在什麼情況下,會觸發form進行HTTP POST,此處設定當任何input tag有變動時。
  • hx_swap對應HTMX中的hx-swap,其功用是指定所需替換的部份,此處使用outerHTML來指定將會完全替換hx-target對象的所有HTML。可以理解為當form每次被觸發後,都會將新的HTML結果(新表格)完全取代id為「"gt"」的tag(舊表格)。

Form產生的instance將用以設定網頁上所需出現的元素,其接受一個Grid。在Grid中:

  • 頭尾各有一個Div來調整整體間距。
  • 中間加入兩個Card來代表兩種顏色的版面。
  • 每個Card中有一個H2(對應HTML中h2 tag)來作為顏色選擇器的標題及一個Input(對應HTML中input tag)來作為顏色選擇器的widget。

此處的GridCard是FastHTML所提供,用以方便排版的元素,預設為PicoCSS風格。

此外,Form內其實可以接受HTML tag或FastHTML component,也就是說我們可以將Gird移至Form內。這邊只是展示其中一種寫法,您可以依照自己的喜好來選擇程式風格。

步驟6:啟動Uvicorn server

於命令列中執行下列指令:

uvicorn app:app --reload

接著打開瀏覽器前往預設網址,如http://127.0.0.1:8000/,就可以見到下面這個漂亮的表格:

table gif

基礎測試

這裡我們建立test_simple_app()來測試:

  • FastHTML中設定的title tag是否為「"FastHTML-GT testing app"」。
  • 在使用@gt2fasthtml的情況下,表格的id是否為「"gt"」。
# https://github.com/jrycw/ft-gt-demo/blob/master/tests/test_simple_app.py
import pytest
from fasthtml.common import FastHTML, Title
from starlette.testclient import TestClient

from ft_gt import gt2fasthtml


@pytest.fixture
def simple_app(gtbl):
    @gt2fasthtml(id="gt")
    def get_tbl():
        return gtbl

    app = FastHTML()

    @app.get("/")
    def homepage():
        div_comp = get_tbl()
        return Title("FastHTML-GT testing app"), div_comp

    yield app


@pytest.fixture
def client(simple_app):
    yield TestClient(simple_app)


def test_simple_app(client):
    resp = client.get("/")
    resp_text = resp.text

    assert "FastHTML-GT testing app" in resp_text
    assert '<div id="gt">' in resp_text

使用Playwright執行互動式測試

首先建立run_server()函數,其功用為呼叫uvicorn.run()啟動伺服器。接著建立一個名為start_server的pytest fixtxre(autouse設為True),其功用為於測試開始時,於背景自動呼叫run_server(),並於測試結束時自動關閉。

# https://github.com/jrycw/ft-gt-demo/blob/master/tests/test_app.py
from multiprocessing import Process

import pytest
import uvicorn
from playwright.sync_api import expect, sync_playwright

from app import app  # FastHTML app is in app.py

test_schema = "http"
test_app_loc = "app:app"
test_host = "127.0.0.1"
test_port = 8741
test_url = f"{test_schema}://{test_host}:{test_port}/"


def run_server():
    uvicorn.run(
        test_app_loc, host=test_host, port=test_port, log_level="info"
    )


@pytest.fixture(scope="module", autouse=True)
def start_server():
    process = Process(target=run_server, daemon=True)
    process.start()
    yield
    process.terminate()

接著建立test_app()函數,來模擬使用chromiumfirefox兩種瀏覽器下,我們的表格是否可以順利調整背景顏色。此外,我們也順便測試了一下titleh1tag的設置是否正確。

# https://github.com/jrycw/ft-gt-demo/blob/master/tests/test_app.py
@pytest.mark.parametrize("core", ["chromium", "firefox"])
def test_app(core):
    with sync_playwright() as p:
        browser = getattr(p, core).launch(headless=True)
        context = browser.new_context(record_video_dir="videos/")
        page = context.new_page()
        page.goto(test_url)

        # test title tag
        expect(page).to_have_title("FastHTML-GT Website")

        # test h1 tag
        first_h1_locator = page.locator("h1").first
        expect(first_h1_locator).to_have_text(
            "Great Tables shown in FastHTML"
        )
        expect(first_h1_locator).to_have_css("text-align", "center")

        color1_value, color2_value = "#663399", "#ffa500"
        color_picker1, color_picker2 = (
            page.locator("#color1"),
            page.locator("#color2"),
        )

        # test initial colors
        expect(color_picker1).to_have_value(color1_value)
        expect(color_picker2).to_have_value(color2_value)

        # change color1
        color_picker1.fill(color2_value)
        expect(color_picker1).to_have_value(color2_value)

        # change color2
        color_picker2.fill(color1_value)
        expect(color_picker2).to_have_value(color1_value)

        context.close()
        browser.close()

最後,可以點選下圖觀看此測試影片
playwright test video

Code

動態表格及其測試可參考ft-gt-demo repo。


上一篇
[Day28] - 如何與FastHTML整合 - 靜態表格
下一篇
[Day30] - 結語及Q&A
系列文
眾裏尋它:Python表格利器Great Tables30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言