Pizza order example#
Example illustrating apps that involve multiple stages
import enum
import json
import time
import threading
import traitlets
import ipywidgets as ipw
from aiidalab_widgets_base import WizardAppWidget
from aiidalab_widgets_base import WizardAppWidgetStep
OrderStatus = enum.Enum(
"OrderStatus",
{"init": 0, "in_preparation": 40, "in_transit": 65, "delivered": 100, "unavailable": -1}
)
class OrderTracker(traitlets.HasTraits):
"""Helper class to keep track of our pizza order."""
configuration = traitlets.Dict() # the pizza configuration that was ordered
status = traitlets.Enum(OrderStatus)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def simulate_order():
self.status = OrderStatus.in_preparation
time.sleep(5)
# We simulate a runtime failure by pretending that "anchovies" are out-of-stock.
if "anchovies" in self.configuration.get("toppings", []):
self.status = OrderStatus.unavailable
return
self.status = OrderStatus.in_transit
time.sleep(3)
self.status = OrderStatus.delivered
threading.Thread(target=simulate_order).start()
@traitlets.default("status")
def _default_status(self):
"""Initialize the initial (default) order status."""
return OrderStatus.init
class OrderProgressWidget(ipw.HBox):
"""Widget to nicely represent the order status."""
status = traitlets.Instance(OrderStatus)
def __init__(self, **kwargs):
self._progress_bar = ipw.FloatProgress(style={"description_width": "initial"}, description="Delivery progress:")
self._status_text = ipw.HTML()
super().__init__([self._progress_bar, self._status_text], **kwargs)
@traitlets.observe("status")
def _observe_status(self, change):
with self.hold_trait_notifications():
if change["new"]:
self._status_text.value = {
OrderStatus.init: "Order received.",
OrderStatus.in_preparation: "In preparation",
OrderStatus.in_transit: "Driver is on their way.",
OrderStatus.delivered: "Delivered! :)",
OrderStatus.unavailable: "Your order is not available",
}.get(change["new"], change["new"].name)
self._progress_bar.value = change["new"].value
self._progress_bar.bar_style = {
OrderStatus.delivered: "success",
OrderStatus.unavailable: "danger",
}.get(change["new"], "info")
else:
self._status_text.value = ""
self._progress_bar.value = 0
self._progress_bar.style = "info"
class ConfigureOrderStep(ipw.HBox, WizardAppWidgetStep):
disabled = traitlets.Bool()
configuration = traitlets.Dict(allow_none=True)
def __init__(self, **kwargs):
# Setup widgets for the pizza configuration
self.style = ipw.RadioButtons(
options=["Neapolitan", "Chicago", "New York-Style", "Detroid"],
description="Style:",
value=None,
)
self.style.observe(self._update_state, ["value"])
self.toppings = ipw.SelectMultiple(
options=["pepperoni", "pineapple", "anchovies"],
description="Toppings:",
)
self.toppings.observe(self._update_state, ["value"])
# Clicking on the "Confirm configuration" button locks the
# current configuration and enables the "order" step.
# The pizza configuration is exposed as the "configuration" trait.
self.confirm_button = ipw.Button(
description="Confirm configuration", disabled=True
)
self.confirm_button.on_click(self._confirm_configuration)
# We need to update the step's state whenever the configuration is changed.
self.observe(self._update_state, ["configuration"])
super().__init__([self.style, self.toppings, self.confirm_button], **kwargs)
def _confirm_configuration(self, button):
"Confirm the pizza configuration and expose as trait."
self.configuration = dict(style=self.style.value, toppings=self.toppings.value)
def reset(self):
with self.hold_trait_notifications():
self.style.value = None
self.toppings.value = []
self.configuration = {}
@traitlets.default("state")
def _default_state(self):
return self.State.READY
def _update_state(self, _=None):
"""Update the step's state based on traits and widget state.
The step state influences the representation of the step (e.g. the "icon") and
whether the "Next step" button is enabled.
"""
if self.configuration:
# The configuration is non-empty, we can move on to the next step.
self.state = self.State.SUCCESS
elif self.style.value and self.toppings.value:
# Both style and topping selection has been made, the step is considered
# to be in the "configured" state. This enables the "Confirm configuration"
# button.
self.state = self.State.CONFIGURED
else:
# In all other cases the step is always considered to be in the "init" state.
self.state = self.State.READY
@traitlets.observe("state")
def _observe_state(self, change):
# Enable the confirm button in case that the pizza has been configured.
with self.hold_trait_notifications():
self.disabled = change["new"] == self.State.SUCCESS
self.confirm_button.disabled = change["new"] is not self.State.CONFIGURED
@traitlets.observe("disabled")
def _observe_disabled(self, change):
with self.hold_trait_notifications():
for child in self.children:
child.disabled = change["new"]
class ReviewAndSubmitOrderStep(ipw.VBox, WizardAppWidgetStep):
# We use traitlets to connect the different steps.
# Note that we can use dlinked transformations, they do not need to be of the same type.
configuration = traitlets.Dict()
# We will keep track of the order status with this Enum trait.
order = traitlets.Instance(OrderTracker, allow_none=True)
def __init__(self, **kwargs):
# The pizza configuration is represented as a formatted dictionary.
self.configuration_label = ipw.HTML()
# The second step has only function: executing the order by clicking on this button.
self.order_button = ipw.Button(description="Submit order", disabled=True)
self.order_button.on_click(self.submit_order)
# We update the step's state whenever there is a change to the configuration or the order status.
self.observe(self._update_state, ["configuration", "order"])
super().__init__([self.configuration_label, self.order_button], **kwargs)
def reset(self):
self.order = None
@traitlets.observe("configuration")
def _observe_configuration(self, change):
"Format and show the pizza configuration."
if change["new"]:
self.configuration_label.value = f"<h4>Configuration</h4><pre>{json.dumps(change['new'], indent=2)}</pre>"
else:
self.configuration_label.value = (
"<h4>Configuration</h4>[Please configure your pizza]"
)
def submit_order(self, button):
"Submit the order and simulate the delivery."
self.order = OrderTracker(configuration=self.configuration)
def _update_state(self, _=None):
"Update the step's state based on the order status and configuration traits."
if self.order: # the order has been submitted
self.state = self.State.SUCCESS
elif self.configuration: # the order can be submitted
self.state = self.State.CONFIGURED
else:
self.state = self.State.INIT
@traitlets.observe("state")
def _observe_state(self, change):
"""Enable the order button once the step is in the "configured" state."""
self.order_button.disabled = change["new"] != self.State.CONFIGURED
class TrackOrderStep(ipw.VBox, WizardAppWidgetStep):
# We receive the order from the previous step and then display information about
# its state in this widget.
order = traitlets.Instance(OrderTracker, allow_none=True)
def __init__(self, **kwargs):
# We will represent the current order status with this dedicated widget.
# It is typically a good idea to strictly separate application logic and
# its representation within the user interface.
self.status_widget = OrderProgressWidget()
super().__init__([self.status_widget], **kwargs)
@traitlets.observe("order")
def _observe_order(self, change):
"Couple a newly received order to the status output widget."
# Note: Technically we must unlink old orders, but this is ommitted for brevity.
if change["new"]:
# Order received, immediate state change:
self.state = self.State.ACTIVE
# Now follow the order status for subsequent state changes:
change["new"].observe(self._update_state, ["status"])
# Link the order status to the widget displaying it to the user:
ipw.dlink((change["new"], "status"), (self.status_widget, "status"))
else:
self.state = self.State.INIT
def can_reset(self):
"Do not allow reset during active order processing."
return self.state is not self.State.ACTIVE
def _update_state(self, change=None):
"Update the step's state based on the order status configuration traits."
new_status = change["new"]
if new_status in (OrderStatus.in_preparation, OrderStatus.in_transit):
self.state = self.State.ACTIVE
elif new_status is OrderStatus.delivered:
self.state = self.State.SUCCESS
elif new_status is OrderStatus.unavailable:
self.state = self.State.FAIL
else:
self.state = self.State.INIT
# Setup all steps of the app. Setting the `auto_advance` trait to True makes it
# so that the next step is automatically selected once the previous step is
# in the "success" state.
configure_order_step = ConfigureOrderStep(auto_advance=True)
review_and_submit_order_step = ReviewAndSubmitOrderStep(auto_advance=True)
track_order_step = TrackOrderStep()
# Data that is communicated from one step to the next via traits.
ipw.dlink(
(configure_order_step, "configuration"),
(review_and_submit_order_step, "configuration"),
)
ipw.dlink((review_and_submit_order_step, "order"), (track_order_step, "order"))
# Setup the app by adding the various steps in order.
app = WizardAppWidget(
steps=[
("Configure pizza", configure_order_step),
("Confirm order", review_and_submit_order_step),
("Track order", track_order_step),
]
)
# Display the app to the user.
display(app)