Chapter 09

注文一覧ページ

alivelimb
alivelimb
2022.05.26に更新

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

注文一覧画面gif

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

No 分類 内容
1 Model BaseDataModel を継承したデータクラス Order, OrderDetail を作成する
2 Service OrderAPIClient を作成する
3 Page PageId を定義し、BasePage を継承したページクラスを作る
4 Application MultiPageApp のpagesにページ ID とページクラスのペアを追加する

Model: データクラス Order, OrderDetail を作成する

データクラス Order, OrderDetail を作成します。Order は注文日時や合計金額など、注文情報を保持します。OrderDetail は注文した商品、数量などを保持します。

Order

まずはデータクラス Order を実装します。注文情報として、注文 ID, ユーザ ID、合計金額、注文日時、注文詳細を保持します。

@dataclass(frozen=True)
class Order(BaseDataModel):
    order_id: str
    user_id: str
    total_price: int
    ordered_at: datetime
    details: list[OrderDetail]

    def to_dict(self) -> dict[str, str]:
        details = [detail.to_dict() for detail in self.details]
        return dict(
            order_id=self.order_id,
            user_id=self.user_id,
            total_price=str(self.total_price),
            ordered_at=self.ordered_at.isoformat(),
            details=json.dumps(details),
        )

    @classmethod
    def from_dict(cls, data: dict[str, str]) -> Order:
        details = [OrderDetail.from_dict(detail_dict) for detail_dict in json.loads(data["details"])]
        return Order(
            order_id=data["order_id"],
            user_id=data["user_id"],
            total_price=int(data["total_price"]),
            ordered_at=datetime.fromisoformat(data["ordered_at"]),
            details=details,
        )

OrderDetail

次に OrderDetail です。注文詳細情報として、注文 No、注文商品、数量、小計を保持します。

@dataclass(frozen=True)
class OrderDetail(BaseDataModel):
    order_no: int
    item: Item
    quantity: int
    subtotal_price: int

    def to_dict(self) -> dict[str, str]:
        item_dict = self.item.to_dict()
        return dict(
            order_no=str(self.order_no),
            item=json.dumps(item_dict),
            quantity=str(self.quantity),
            subtotal_price=str(self.subtotal_price),
        )

    @classmethod
    def from_dict(cls, data: dict[str, str]) -> OrderDetail:
        item_dict = json.loads(data["item"])
        return OrderDetail(
            order_no=int(data["order_no"]),
            item=Item.from_dict(item_dict),
            quantity=int(data["quantity"]),
            subtotal_price=int(data["subtotal_price"]),
        )

Service: CartAPIClient を作成する

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

class IOrderAPIClientService(Protocol):
    def get_orders(self, session_id: str) -> list[Order]:
        pass

    def order_commit(self, session_id: str) -> None:
        pass

今回は以下のメソッドを定義しました。

No メソッド名 機能
1 get_orders セッション ID を指定してログインユーザの注文一覧を取得する
2 order_commit セッション ID を指定してカート商品の注文を確定する

get_orders

まずはget_ordersから実装していきましょう。

class MockOrderAPIClientService(IOrderAPIClientService):
    def __init__(self, mockdb: MockDB, session_db: MockSessionDB) -> None:
        self.mockdb = mockdb
        self.session_db = session_db

    def get_orders(self, session_id: str) -> list[Order]:
        session = self._get_session(session_id)

        with self.mockdb.connect() as db:
            orders_table: dataset.Table = db["orders"]
            orders_data = list(orders_table.find(user_id=session.user_id))
            orders = [Order.from_dict(json.loads(order_data["order_body"])) for order_data in orders_data]

        return orders

    def _get_session(self, session_id: str) -> Session:
        with self.session_db.connect() as db:
            query = Query()
            doc = db.search(query.session_id == session_id)[0]
            session = Session.from_dict(doc)

        return session

_get_sessionでセッション ID から Session を取得し、get_ordersでユーザの注文一覧を取得します。

order_commit

次にorder_commitを実装します。

JST = timezone(timedelta(hours=+9), "JST") # (A)


class MockOrderAPIClientService(IOrderAPIClientService):
    # (中略)

    def order_commit(self, session_id: str) -> None:
        session = self._get_session(session_id)

        order = self._create_order_from_cart(session.cart)
        with self.mockdb.connect() as db:
            # 注文テーブル
            orders_table: dataset.Table = db["orders"]
            order_data = dict(
                order_id=order.order_id,
                user_id=order.user_id,
                order_body=json.dumps(order.to_dict(), default=str),
            )
            orders_table.insert(order_data)

    def _create_order_from_cart(self, cart: Cart) -> Order:
        order_details = []
        for idx, cart_item in enumerate(cart.cart_items):
            subtotal_price = cart_item.item.price * cart_item.quantity
            order_detail = OrderDetail(
                order_no=idx + 1,
                item=cart_item.item,
                quantity=cart_item.quantity,
                subtotal_price=subtotal_price,
            )
            order_details.append(order_detail)
        order = Order(
            order_id=str(uuid4()),
            user_id=cart.user_id,
            total_price=cart.total_price,
            ordered_at=datetime.now(JST), # (B)
            details=order_details,
        )
        return order

_create_order_from_cartで Cart から Order を生成します。コード中(A)で日本のタイムゾーンを定義し、コード中(B)で注文時刻を日本時間での現在時刻としています。

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

これまでと同じ手順でページを実装していきましょう。

PageId の定義

まずは PageId の定義です。注文一覧ページを閲覧するにはログインが必要なので、プレフィックスをMEMBERとしておきます。

class PageId(Enum):
    # (中略)
    MEMBER_CART = auto()
    MEMBER_ORDER_LIST = auto() # 追加

ページクラスの作成

次に注文一覧ページを実装していきます。カートページと同様MemberPageを継承して実装します。

class OrderListPage(MemberPage):
    def render(self) -> None:
        session_id = self.ssm.get_session_id()
        if not self.validate_user() or session_id is None:
            return

        order_api_client = self.ssm.get_order_api_client()

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

        # カートテーブル表示
        col_size = [1, 2, 2, 4, 2]
        columns = st.columns(col_size)
        headers = ["No", "注文ID", "合計金額", "注文日付", ""]
        for col, field_name in zip(columns, headers):
            col.write(field_name)

        # 注文一覧を取得
        try:
            orders = order_api_client.get_orders(session_id)
        except NotFoundError:
            st.warning("注文履歴はありません。")
            return

        for index, order in enumerate(orders):
            (
                col_no,
                col_id,
                col_total,
                col_date,
                col_button,
            ) = st.columns(col_size)
            col_no.write(index + 1)
            col_id.write(order.order_id[-8:]) # (A)
            col_total.write(order.total_price)
            col_date.write(order.ordered_at.strftime("%Y-%m-%d %H:%M:%S"))
            col_button.button("詳細", key=order.order_id, on_click=self._order_detail, args=(order,)) # (B)

    def _order_detail(self, order: Order) -> None:
        self.ssm.set_order(order)
        self.ssm.set_page_id(PageId.MEMBER_ORDER_DETAIL)

注文 ID を全て表示すると長いので、コード中(A)で下 8 桁のみを表示しています。

コード中(B)ではページ ID を変更するため、コールバック関数を利用する必要があります。

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

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

def init_pages(ssm: StreamlitSessionManager) -> list[BasePage]:
    pages = [
        # (中略)
        CartPage(page_id=PageId.MEMBER_CART, title="カート", ssm=ssm),
        OrderListPage(page_id=PageId.MEMBER_ORDER_LIST, title="注文一覧", ssm=ssm), # 追加
    ]
    return pages

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