Chapter 06

商品一覧ページ

alivelimb
alivelimb
2022.05.25に更新

本章では商品一覧ページを実装していきます。完成イメージは以下の通りです。

商品一覧画面gif

作成手順は User と同様で以下の流れで行います。

No 分類 内容
1 Model BaseDataModel を継承したデータクラス Item を作成する
2 MockDB 商品テーブルを作成し、ダミーデータを生成・追加する
3 Service ItemAPIClient を作成する
4 Page PageId を定義し、BasePage を継承したページクラスを作る
5 Application MultiPageApp のpagesにページ ID とページクラスのペアを追加する

Model: データクラス Item を作成する

User, Session と同様に、データクラス Item を実装していきます。商品情報として商品 ID, 商品名、価格、生産地を保持します。実装は以下の通りです。

@dataclass(frozen=True)
class Item:
    item_id: str
    name: str
    price: int
    producing_area: str

    def to_dict(self) -> dict[str, str]:
        return dict(
            item_id=self.item_id,
            name=self.name,
            price=str(self.price),
            producing_area=self.producing_area,
        )

    @classmethod
    def from_dict(cls, data: dict[str, str]) -> Item:
        return Item(
            item_id=data["item_id"],
            name=data["name"],
            price=int(data["price"]),
            producing_area=data["producing_area"],
        )

MockDB: 商品テーブルを作成し、ダミーデータを生成・追加する

次に商品テーブルを作成します。テーブル作成は User とほとんど同じですが、商品テーブルではダミーデータの作成にMimesisを用います。また価格を「100 x N - 2」にすることでリアルさを出しています。

class MockDB:
    # (中略)

    def _init_mock_db(self) -> None:
        self._create_mock_user_table()
        self._create_mock_item_table()

    def _create_mock_item_table(self, n: int = 10) -> None:
        _ = Field(locale=Locale.JA)
        schema = Schema(
            schema=lambda: {
                "item_id": _("uuid"),
                "name": _("vegetable"),
                "price": randint(1, 5) * 100 - 2,
                "producing_area": _("prefecture"),
            }
        )
        mock_items = [
            Item(
                item_id=data["item_id"],
                name=data["name"],
                price=data["price"],
                producing_area=data["producing_area"],
            )
            for data in schema.create(n)
        ]
        with self.connect() as db:
            table: dataset.Table = db["items"]
            for mock_item in mock_items:
                table.insert(mock_item.to_dict())

ダミーデータの作成以外は User とほぼ同じなので、説明は不要かと思います。

Service: ItemAPIClient を作成する

こちらも User とほとんど同様です。まずは IF(インターフェース)を定義します。

class IItemAPIClientService(Protocol):
    def get_all(self) -> list[Item]:
        pass

今回は全商品を取得するget_allメソッドだけを用意します。

では MockItemAPIClientService を実装していきましょう。

class MockItemAPIClientService(IItemAPIClientService):
    def __init__(self, mockdb: MockDB) -> None:
        self.mockdb = mockdb

    def get_all(self) -> list[Item]:
        with self.mockdb.connect() as db:
            table: dataset.Table = db["items"]
            items_data = table.all()

        return [Item.from_dict(item_data) for item_data in items_data]

UserAPIClient とほとんど同じですが、商品一覧はログインしているかに関係なく取得できるため、ItemAPIClient では MockSessionDB を持ちません。

Page: PageId を定義し、ページクラスを作る

User と同じように以下の手順でページを追加しています。

  1. PageId の属性を増やす
  2. BasePage を継承したページクラスを作る
  3. MultiPageApp のpagesにページ ID とページクラスのペアを追加する

PageId の定義

商品一覧ページもログイン状態に関係なくアクセス出来るページなので、PUBLIC_をプレフィックスとしておきましょう。

class PageId(Enum):
    PUBLIC_LOGIN = auto()
    PUBLIC_ITEM_LIST = auto() # 追加

ページクラスの追加

class ItemListPage(BasePage):
    def render(self) -> None:
        item_api_client: IItemAPIClientService = self.ssm.get_item_api_client()

        # タイトルの表示
        st.title(self.title)

        # 商品テーブルの表示
        col_size = [1, 2, 2, 2] # (A)
        columns = st.columns(col_size)
        headers = ["No", "名前", "価格", ""]
        for col, field_name in zip(columns, headers):
            col.write(field_name)

        for index, item in enumerate(item_api_client.get_all()): # (B)
            (
                no_col,
                name_col,
                price_col,
                button_col,
            ) = st.columns(col_size)
            no_col.write(index + 1)
            name_col.write(item.name)
            price_col.write(item.price)
            button_col.button("詳細", key=item.item_id, on_click=self._detail_on_clink, args=(item,)) # (B)

    def _detail_on_clink(self, item: Item) -> None:
        self.ssm.set_item(item)
        self.ssm.set_page_id(PageId.PUBLIC_ITEM_DETAIL)

コード中(A)では商品テーブル(DB ではなく、描画する表)の列数、各列の幅比率を決めています。商品テーブルでは No, 名前(商品名), 価格, 詳細ボタン列の 4 列としています。

取得した商品一覧を for 文で回し、商品毎に商品レコードの行を作成しています。コード中(B)では各行の詳細ボタンを作成しています。詳細ボタンがクリックされると、ページを遷移させたいためset_page_idでページ ID を変更しています。page_id は Application の選択ボックスで定義されています。

class MultiPageApp:
    # (中略)

    def render(self) -> None:
        # ページ選択ボックスを追加
        page_id = st.sidebar.selectbox(
            self.nav_label,
            list(self.pages.keys()),
            format_func=lambda page_id: self.pages[page_id].title,
            key=SessionKey.PAGE_ID.name, # keyでPageIDを指定している
        )

さて、覚えていますでしょうか。selectbox のような入力を受け付けるウィジェットの値を操作する場合、if 分ではなくコールバック関数を使う必要がありました。(覚えていない方は Streamlit の章を見直してみて下さい)

そのため、ItemListPage のコード中(B)ではon_clickにコールバック関数_detail_on_clickを渡しています。また、コールバック関数に位置引数を渡したい時はargs, キーワード引数を渡したい時はkwargsを指定します。

Application: ページ ID とページクラスのペアを追加する

最後に作成したページを Application に追加します。

def init_pages(ssm: StreamlitSessionManager) -> list[BasePage]:
    pages = [
        LoginPage(page_id=PageId.PUBLIC_LOGIN, title="ログイン", ssm=ssm),
        ItemListPage(page_id=PageId.PUBLIC_ITEM_LIST, title="商品一覧", ssm=ssm), # 追加
    ]
    return pages

SSM, SessionKey 等も登録する必要がありますが、ここでの紹介は省略するため、ソースコードを確認してください。