From af0288088717266dd2d18b39725eb69c3469a33a Mon Sep 17 00:00:00 2001 From: Sn3llius Date: Wed, 10 Apr 2024 18:18:38 +0200 Subject: [PATCH] add CRUD tutorial --- .../project-template-Simple CRUD/README.md | 1 + .../project-template-Simple CRUD/__init__.py | 0 .../components/__init__.py | 2 + .../components/item_editor.py | 144 ++++++++++++++ .../components/item_list.py | 116 ++++++++++++ .../project-template-Simple CRUD/models.py | 76 ++++++++ .../pages/__init__.py | 1 + .../pages/crud_page.py | 178 ++++++++++++++++++ .../thumbnail.svg | 97 ++++++++++ 9 files changed, 615 insertions(+) create mode 100644 rio/snippets/snippet-files/project-template-Simple CRUD/README.md create mode 100644 rio/snippets/snippet-files/project-template-Simple CRUD/__init__.py create mode 100644 rio/snippets/snippet-files/project-template-Simple CRUD/components/__init__.py create mode 100644 rio/snippets/snippet-files/project-template-Simple CRUD/components/item_editor.py create mode 100644 rio/snippets/snippet-files/project-template-Simple CRUD/components/item_list.py create mode 100644 rio/snippets/snippet-files/project-template-Simple CRUD/models.py create mode 100644 rio/snippets/snippet-files/project-template-Simple CRUD/pages/__init__.py create mode 100644 rio/snippets/snippet-files/project-template-Simple CRUD/pages/crud_page.py create mode 100644 rio/snippets/snippet-files/project-template-Simple CRUD/thumbnail.svg diff --git a/rio/snippets/snippet-files/project-template-Simple CRUD/README.md b/rio/snippets/snippet-files/project-template-Simple CRUD/README.md new file mode 100644 index 00000000..30404ce4 --- /dev/null +++ b/rio/snippets/snippet-files/project-template-Simple CRUD/README.md @@ -0,0 +1 @@ +TODO \ No newline at end of file diff --git a/rio/snippets/snippet-files/project-template-Simple CRUD/__init__.py b/rio/snippets/snippet-files/project-template-Simple CRUD/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rio/snippets/snippet-files/project-template-Simple CRUD/components/__init__.py b/rio/snippets/snippet-files/project-template-Simple CRUD/components/__init__.py new file mode 100644 index 00000000..8843b585 --- /dev/null +++ b/rio/snippets/snippet-files/project-template-Simple CRUD/components/__init__.py @@ -0,0 +1,2 @@ +from .item_editor import ItemEditor +from .item_list import ItemList diff --git a/rio/snippets/snippet-files/project-template-Simple CRUD/components/item_editor.py b/rio/snippets/snippet-files/project-template-Simple CRUD/components/item_editor.py new file mode 100644 index 00000000..13bbc569 --- /dev/null +++ b/rio/snippets/snippet-files/project-template-Simple CRUD/components/item_editor.py @@ -0,0 +1,144 @@ +import rio + + +# +from .. import models + +# + + +# +class ItemEditor(rio.Component): + """ + A component for editing a menu item. + + Returns a card component containing the menu item editor. The editor contains fields + for name, description, price, and category of the menu item. The component also contains + buttons for saving or canceling the changes. + + Attributes: + currently_selected_menu_item: The currently selected menu item. + new_entry: A boolean flag indicating if the menu item is a new entry. + on_cancel_event: An event handler for the cancel button. + on_save_event: An event handler for the save button. + """ + + currently_selected_menu_item: models.MenuItems + new_entry: bool = False + on_cancel_event: rio.EventHandler[[]] = None + on_save_event: rio.EventHandler[[]] = None + + async def on_press_save_event(self) -> None: + """ + Asynchronously triggers the 'on_save_event' when the save button is pressed. + """ + await self.call_event_handler(self.on_save_event) + + async def on_press_cancel_event(self) -> None: + """ + Asynchronously triggers the 'on_cancel_event' when the cancel button is pressed. + """ + await self.call_event_handler(self.on_cancel_event) + + def on_change_name(self, ev: rio.TextInputChangeEvent) -> None: + """ + Changes the name of the currently selected menu item. + + Args: + ev: The event object that contains the new text. + """ + self.currently_selected_menu_item.name = ev.text + + def on_change_description(self, ev: rio.TextInputChangeEvent) -> None: + """ + Changes the description of the currently selected menu item. + + Args: + ev: The event object that contains the new text. + """ + self.currently_selected_menu_item.description = ev.text + + def on_change_price(self, ev: rio.NumberInputChangeEvent) -> None: + """ + Changes the price of the currently selected menu item. + + Args: + ev: The event object that contains the new price. + """ + self.currently_selected_menu_item.price = ev.value + + def on_change_category(self, ev: rio.DropdownChangeEvent) -> None: + """ + Changes the category of the currently selected menu item. + + Args: + ev: The event object that contains the new category. + """ + self.currently_selected_menu_item.category = ev.value + + def build(self) -> rio.Component: + """ + Builds the menu item editor component. + + Returns: + A card component containing the menu item editor. + See the approx. layout below: + + ################ Card ################# + # Text # + # TextInput (Name) # + # TextInput (Description) # + # NumberInput (Price) # + # Dropdown (Category) # + # Button (Save) | Button (Cancel) # + ####################################### + """ + + if self.new_entry is False: + text = "Edit Menu Item" + else: + text = "Add New Menu Item" + + return rio.Card( + rio.Column( + rio.Text( + text=text, + style="heading2", + margin_bottom=1, + ), + rio.TextInput( + self.currently_selected_menu_item.name, + label="Name", + on_change=self.on_change_name, + ), + rio.TextInput( + self.currently_selected_menu_item.description, + label="Description", + on_change=self.on_change_description, + ), + rio.NumberInput( + self.currently_selected_menu_item.price, + label="Price", + suffix_text="$", + on_change=self.on_change_price, + ), + rio.Dropdown( + options=["Burgers", "Desserts", "Drinks", "Salads", "Sides"], + label="Category", + selected_value=self.currently_selected_menu_item.category, + on_change=self.on_change_category, + ), + rio.Row( + rio.Button("Save", on_press=self.on_press_save_event), + rio.Button("Cancel", on_press=self.on_press_cancel_event), + spacing=1, + align_x=1, + ), + spacing=1, + align_y=0, + margin=2, + ), + ) + + +# diff --git a/rio/snippets/snippet-files/project-template-Simple CRUD/components/item_list.py b/rio/snippets/snippet-files/project-template-Simple CRUD/components/item_list.py new file mode 100644 index 00000000..823fff61 --- /dev/null +++ b/rio/snippets/snippet-files/project-template-Simple CRUD/components/item_list.py @@ -0,0 +1,116 @@ +import rio + +# +import functools +from .. import models + +# + + +# +class ItemList(rio.Component): + """ + A component for displaying a list of menu items. + + Returns a list of menu items in a ListView component. Each item in the list contains the name, + description, and a delete button. The delete button triggers an event to delete the item. + + Attributes: + menu_item_set: The list of menu items to be displayed. + on_add_new_item_event: An event handler for adding a new item. + on_delete_item_event: An event handler for deleting an item. + on_select_item_event: An event handler for selecting an item. + """ + + menu_item_set: list[models.MenuItems] + on_add_new_item_event: rio.EventHandler[[]] = None + on_delete_item_event: rio.EventHandler[int] = None + on_select_item_event: rio.EventHandler[models.MenuItems] = None + + async def on_press_add_new_item_event(self) -> None: + """ + Asynchronously triggers the 'add new item' when the list item is pressed. + """ + await self.call_event_handler(self.on_add_new_item_event) + + async def on_press_delete_item_event(self, idx: int) -> None: + """ + Asynchronously triggers the 'delete item' when the delete button is pressed. + The event handler is passed the index of the item to be deleted. + + Args: + idx: The index of the item to be deleted. + """ + await self.call_event_handler(self.on_delete_item_event, idx) + # update the list + await self.force_refresh() + + async def on_press_select_item_event(self, item: models.MenuItems) -> None: + """ + Asynchronously triggers the 'select item' when an item is selected. + The event handler is passed the selected item. + + Args: + item: The selected item. + """ + await self.call_event_handler(self.on_select_item_event, item) + + def build(self) -> rio.Component: + """ + Builds the component by returning a ListView component containing the menu items. + + Returns: + A ListView component containing the menu items. + See the approx. layout below: + + ############### ListView ############### + # + Add new # + # Hamburger Button(Delete) # + # Cheese Burger Button(Delete) # + # Fries Button(Delete) # + # ... # + ######################################## + """ + + # Store all children in an intermediate list + list_items = [] + + list_items.append( + rio.SimpleListItem( + text="Add new", + secondary_text="Description", + key="add_new", + left_child=rio.Icon("material/add"), + on_press=self.on_press_add_new_item_event, + ) + ) + + for i, item in enumerate(self.menu_item_set): + list_items.append( + rio.SimpleListItem( + text=item.name, + secondary_text=item.description, + right_child=rio.Button( + rio.Icon("material/delete"), + color=self.session.theme.danger_color, + # Note the use of functools.partial to pass the + # index to the event handler. + on_press=functools.partial(self.on_press_delete_item_event, i), + ), + # Use the name as the key to ensure that the list item + # is unique. + key=item.name, + # Note the use of functools.partial to pass the + # item to the event handler. + on_press=functools.partial(self.on_press_select_item_event, item), + ) + ) + + # Then unpack the list to pass the children to the ListView + return rio.ListView( + *list_items, + align_y=0, + ) + + +# diff --git a/rio/snippets/snippet-files/project-template-Simple CRUD/models.py b/rio/snippets/snippet-files/project-template-Simple CRUD/models.py new file mode 100644 index 00000000..4bfc7541 --- /dev/null +++ b/rio/snippets/snippet-files/project-template-Simple CRUD/models.py @@ -0,0 +1,76 @@ +from dataclasses import dataclass +from typing import * # type:ignore + + +@dataclass +class MenuItems: + """ + MenuItems data model. + + Attributes: + name: The name of the menu item. + description: The description of the menu item. + price: The price of the menu item. + category: The category of the menu item. + """ + + name: str + description: str + price: float + category: str + + @staticmethod + def new_empty() -> "MenuItems": + """ + Creates a new empty MenuItems object. + + Returns: + MenuItems: A new empty MenuItems object. + """ + return MenuItems( + name="", + description="", + price=0.0, + category="", + ) + + +# initial data +MENUITEMSET: list[MenuItems] = [ + MenuItems( + name="Hamburger", + description="A classic hamburger with lettuce, tomato, and onions", + price=4.99, + category="Burgers", + ), + MenuItems( + name="Cheeseburger", + description="A classic cheeseburger with lettuce, tomato, onions and cheese", + price=5.99, + category="Burgers", + ), + MenuItems( + name="Fries", + description="A side of crispy fries", + price=2.99, + category="Sides", + ), + MenuItems( + name="Soda", + description="A refreshing soda", + price=1.99, + category="Drinks", + ), + MenuItems( + name="Salad", + description="A fresh salad", + price=4.99, + category="Salads", + ), + MenuItems( + name="Ice Cream", + description="A sweet treat", + price=3.99, + category="Desserts", + ), +] diff --git a/rio/snippets/snippet-files/project-template-Simple CRUD/pages/__init__.py b/rio/snippets/snippet-files/project-template-Simple CRUD/pages/__init__.py new file mode 100644 index 00000000..c1d4daa2 --- /dev/null +++ b/rio/snippets/snippet-files/project-template-Simple CRUD/pages/__init__.py @@ -0,0 +1 @@ +from .crud_page import CrudPage diff --git a/rio/snippets/snippet-files/project-template-Simple CRUD/pages/crud_page.py b/rio/snippets/snippet-files/project-template-Simple CRUD/pages/crud_page.py new file mode 100644 index 00000000..4f8bfa12 --- /dev/null +++ b/rio/snippets/snippet-files/project-template-Simple CRUD/pages/crud_page.py @@ -0,0 +1,178 @@ +import rio + + +# +from .. import components as comps +from .. import models + +# + + +# +class CrudPage(rio.Component): + """ + A CRUD page that allows users to create, read, update, and delete menu items. + + This component is composed of a Banner component, an ItemList component, and an ItemEditor component. + + The @rio.event.on_populate decorator is used to fetch data from a predefined data model and assign it to the + menu_item_set attribute of the current instance. The on_press_delete_item, on_press_cancel_event, and + on_press_save_event methods are used to handle delete, cancel, and save events, respectively. The on_press_add_new_item + method is used to handle the add new item event. The on_press_select_menu_item method is used to handle the selection + of a menu item. + + Attributes: + menu_item_set: A list of menu items. + currently_selected_menu_item: The currently selected menu item. + banner_text: The text to be displayed in the banner. + is_new_entry: A flag to indicate if the currently selected menu item is a new entry. + """ + + menu_item_set: list[models.MenuItems] = [] + currently_selected_menu_item: models.MenuItems | None = None + banner_text: str = "" + is_new_entry: bool = False + + @rio.event.on_populate + def on_populate(self) -> None: + """ + Event handler that is called when the component is populated. + + Fetches data from a predefined data model and assigns it to the menu_item_set + attribute of the current instance. + """ + self.menu_item_set = models.MENUITEMSET + + async def on_press_delete_item(self, idx: int) -> None: + """ + Perform actions when the "Delete" button is pressed. + + Args: + idx: The index of the item to be deleted. + """ + self.menu_item_set.pop(idx) + self.banner_text = "Item was deleted" + self.currently_selected_menu_item = None + + async def on_press_cancel_event(self) -> None: + """ + Perform actions when the "Cancel" button is pressed. + """ + self.currently_selected_menu_item = None + self.banner_text = "" + + def on_press_save_event(self) -> None: + """ + Performs actions when the "Save" button is pressed. + + This method appends the currently selected menu item to the menu item set if it is a new entry, + or updates the menu item set if it is an existing entry. It also updates the banner text and sets + the is_new_entry flag to False. + """ + assert self.currently_selected_menu_item is not None + if self.is_new_entry: + self.menu_item_set.append(self.currently_selected_menu_item) + self.banner_text = "Item was added" + self.is_new_entry = False + self.currently_selected_menu_item = None + else: + self.banner_text = "Item was updated" + + async def on_press_add_new_item(self) -> None: + """ + Perform actions when the "Add New" ListItem is pressed. + + This method sets the currently selected menu item to a new empty instance of models.MenuItems, + clears the banner text, and sets the is_new_entry flag to True. + """ + self.currently_selected_menu_item = models.MenuItems.new_empty() + self.banner_text = "" + self.is_new_entry = True + + async def on_press_select_menu_item( + self, selected_menu_item: models.MenuItems + ) -> None: + """ + Perform actions when a menu item is selected. + + This method sets the currently selected menu item to the selected menu item, + which is passed as an argument. + + Args: + selected_menu_item: The selected menu item. + """ + self.currently_selected_menu_item = selected_menu_item + + def build(self) -> rio.Component: + """ + Builds the component to be rendered. + + If there is no currently selected menu item, only the Banner and ItemList component is returned. + + Otherwise, if there is a currently selected menu item, both the Banner and ItemList component + and the ItemEditor component are returned. + + Returns: + rio.Component: The components to be rendered. + See the approx. layout below: + + ############### Column ############### + # Banner # + # +------------------------------- # + # | ItemList | # + # | | # + # +------------------------------- # + ###################################### + + or + + ############### Column ############### + # Banner # + # +------------------------------- # + # | ItemList | ItemEditor | # + # | | | # + # +------------------------------- # + ###################################### + + """ + + if self.currently_selected_menu_item is None: + return rio.Column( + rio.Banner(self.banner_text, style="danger"), + comps.ItemList( + menu_item_set=self.menu_item_set, + on_add_new_item_event=self.on_press_add_new_item, + on_delete_item_event=self.on_press_delete_item, + on_select_item_event=self.on_press_select_menu_item, + align_y=0, + ), + align_y=0, + margin=3, + ) + else: + return rio.Column( + rio.Banner(self.banner_text, style="danger"), + rio.Row( + comps.ItemList( + menu_item_set=self.menu_item_set, + on_add_new_item_event=self.on_press_add_new_item, + on_delete_item_event=self.on_press_delete_item, + on_select_item_event=self.on_press_select_menu_item, + align_y=0, + ), + comps.ItemEditor( + self.currently_selected_menu_item, + new_entry=self.is_new_entry, + on_cancel_event=self.on_press_cancel_event, + on_save_event=self.on_press_save_event, + ), + spacing=1, + proportions=(2, 1), + ), + spacing=1, + align_y=0, + margin=3, + ) + + +# diff --git a/rio/snippets/snippet-files/project-template-Simple CRUD/thumbnail.svg b/rio/snippets/snippet-files/project-template-Simple CRUD/thumbnail.svg new file mode 100644 index 00000000..48a4248e --- /dev/null +++ b/rio/snippets/snippet-files/project-template-Simple CRUD/thumbnail.svg @@ -0,0 +1,97 @@ + + + + + + + + + + image/svg+xml + + + + + + + TODO: Thumbnail + SimpleDashboard + +