From d56cfce7e5ec41d588d9b8754a689b849f150465 Mon Sep 17 00:00:00 2001 From: gruhn Date: Sat, 19 Oct 2024 15:39:36 +0200 Subject: [PATCH 1/7] add list of list support to buttongroup widget --- example/ui_showcase.py | 6 ++++++ package-lock.json | 2 +- shc/web/templates/widgets/buttongroup.htm | 12 +++++++++--- shc/web/widgets.py | 13 +++++++++---- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/example/ui_showcase.py b/example/ui_showcase.py index 219a6cea..bccfb2d9 100644 --- a/example/ui_showcase.py +++ b/example/ui_showcase.py @@ -63,6 +63,12 @@ class Fruits(enum.Enum): confirm_message="Do you want the foobar?", confirm_values=[True]).connect(foobar), ])) +# A simple ButtonGroup with nested lists of ToggleButtons for foobar +index_page.add_item(ButtonGroup("State of the foobar (grouped)", [ + [ToggleButton("Foo").connect(foo),ToggleButton("Bar", color='red').connect(bar)], + [ToggleButton("Foobar", color='black').connect(foobar)], +])) + # We can also use ValueButtons to represent individual states (here in the 'outline' version) index_page.add_item(ButtonGroup("The Foo", [ ValueButton(False, "Off", outline=True, color="black").connect(foo), diff --git a/package-lock.json b/package-lock.json index 10c1634d..cf0ed637 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "shc2", + "name": "smarthomeconnect", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/shc/web/templates/widgets/buttongroup.htm b/shc/web/templates/widgets/buttongroup.htm index dc23d1a9..2259638a 100644 --- a/shc/web/templates/widgets/buttongroup.htm +++ b/shc/web/templates/widgets/buttongroup.htm @@ -1,7 +1,13 @@ {% from "buttons.inc.htm" import render_button %} -
- {% for button in buttons %} - {{ render_button(button) }} +
+ {% for button_group in button_groups %} +
+
+ {% for button in button_group %} + {{ render_button(button) }} + {% endfor %} +
+
{% endfor %}
diff --git a/shc/web/widgets.py b/shc/web/widgets.py index 2939b372..5f54e595 100644 --- a/shc/web/widgets.py +++ b/shc/web/widgets.py @@ -322,19 +322,24 @@ class ButtonGroup(WebPageItem): `Connectable`. :param label: The label to be shown left of the buttons - :param buttons: List of button descriptors + :param buttons: List or a List if List of button descriptors """ - def __init__(self, label: Union[str, Markup], buttons: Iterable["AbstractButton"]): + def __init__(self, label: Union[str, Markup], buttons: Union[Iterable["AbstractButton"], Iterable[Iterable["AbstractButton"]]]): super().__init__() self.label = label - self.buttons = buttons + if all(isinstance(item, Iterable) for item in buttons): + self.buttons = list(itertools.chain(*buttons)) + self.button_groups = buttons + else: + self.buttons = buttons + self.button_groups = [buttons] def get_connectors(self) -> Iterable[WebUIConnector]: return self.buttons # type: ignore async def render(self) -> str: return await jinja_env.get_template('widgets/buttongroup.htm')\ - .render_async(label=self.label, buttons=self.buttons) + .render_async(label=self.label, button_groups=self.button_groups) class AbstractButton(metaclass=abc.ABCMeta): From cf94d0effe2a09216acb5a5fa42273b282752086 Mon Sep 17 00:00:00 2001 From: gruhn Date: Sat, 19 Oct 2024 16:36:02 +0200 Subject: [PATCH 2/7] remove package-lock.json --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index cf0ed637..10c1634d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "smarthomeconnect", + "name": "shc2", "lockfileVersion": 3, "requires": true, "packages": { From 6a93233d6b6c59d3f86fa9939dcf7965375f89dc Mon Sep 17 00:00:00 2001 From: gruhn Date: Sat, 19 Oct 2024 16:38:57 +0200 Subject: [PATCH 3/7] fix: flake issues --- example/ui_showcase.py | 2 +- shc/web/widgets.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/example/ui_showcase.py b/example/ui_showcase.py index bccfb2d9..922f87ac 100644 --- a/example/ui_showcase.py +++ b/example/ui_showcase.py @@ -65,7 +65,7 @@ class Fruits(enum.Enum): # A simple ButtonGroup with nested lists of ToggleButtons for foobar index_page.add_item(ButtonGroup("State of the foobar (grouped)", [ - [ToggleButton("Foo").connect(foo),ToggleButton("Bar", color='red').connect(bar)], + [ToggleButton("Foo").connect(foo), ToggleButton("Bar", color='red').connect(bar)], [ToggleButton("Foobar", color='black').connect(foobar)], ])) diff --git a/shc/web/widgets.py b/shc/web/widgets.py index 5f54e595..ddba6873 100644 --- a/shc/web/widgets.py +++ b/shc/web/widgets.py @@ -324,7 +324,11 @@ class ButtonGroup(WebPageItem): :param label: The label to be shown left of the buttons :param buttons: List or a List if List of button descriptors """ - def __init__(self, label: Union[str, Markup], buttons: Union[Iterable["AbstractButton"], Iterable[Iterable["AbstractButton"]]]): + def __init__( + self, + label: Union[str, Markup], + buttons: Union[Iterable["AbstractButton"], Iterable[Iterable["AbstractButton"]]], + ): super().__init__() self.label = label if all(isinstance(item, Iterable) for item in buttons): From 97fd8b1c82d5997aa9f135c95c81acfc21270612 Mon Sep 17 00:00:00 2001 From: gruhn Date: Sat, 19 Oct 2024 19:04:42 +0200 Subject: [PATCH 4/7] fix: tests --- example/pulseaudio_sink.py | 7 ++++--- shc/web/widgets.py | 22 +++++++++++++++++----- test/test_web.py | 10 +++++++--- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/example/pulseaudio_sink.py b/example/pulseaudio_sink.py index 5fd0bb66..cf71f60a 100644 --- a/example/pulseaudio_sink.py +++ b/example/pulseaudio_sink.py @@ -13,11 +13,12 @@ A simple SHC example, providing a web interface to control and supervise the default sink of the local Pulseaudio server. """ +from typing import cast, Iterable import shc import shc.web import shc.interfaces.pulse -from shc.web.widgets import Slider, ButtonGroup, ToggleButton, icon, DisplayButton +from shc.web.widgets import AbstractButton, Slider, ButtonGroup, ToggleButton, icon, DisplayButton interface = shc.interfaces.pulse.PulseAudioInterface() sink_name = None @@ -35,10 +36,10 @@ page.add_item(Slider("Volume").connect(volume.field('volume'))) page.add_item(Slider("Balance").connect(volume.field('balance'), convert=True)) page.add_item(Slider("Fade").connect(volume.field('fade'), convert=True)) -page.add_item(ButtonGroup("", [ +page.add_item(ButtonGroup("", cast(Iterable[AbstractButton], [ ToggleButton(icon('volume mute')).connect(mute), DisplayButton(label=icon('power off')).connect(active), -])) +]))) if __name__ == '__main__': shc.main() diff --git a/shc/web/widgets.py b/shc/web/widgets.py index ddba6873..cb2e6fb2 100644 --- a/shc/web/widgets.py +++ b/shc/web/widgets.py @@ -30,7 +30,19 @@ import json import pathlib from os import PathLike -from typing import Any, Type, Union, Iterable, List, Generic, Tuple, TypeVar, Optional, Callable +from typing import ( + Any, + cast, + Type, + Union, + Iterable, + List, + Generic, + Tuple, + TypeVar, + Optional, + Callable, +) import markupsafe from markupsafe import Markup @@ -332,11 +344,11 @@ def __init__( super().__init__() self.label = label if all(isinstance(item, Iterable) for item in buttons): - self.buttons = list(itertools.chain(*buttons)) - self.button_groups = buttons + self.buttons: Iterable["AbstractButton"] = list(itertools.chain(*buttons)) + self.button_groups = cast(Iterable[Iterable["AbstractButton"]], buttons) else: - self.buttons = buttons - self.button_groups = [buttons] + self.buttons = cast(Iterable["AbstractButton"], buttons) + self.button_groups = cast(Iterable[Iterable["AbstractButton"]], [buttons]) def get_connectors(self) -> Iterable[WebUIConnector]: return self.buttons # type: ignore diff --git a/test/test_web.py b/test/test_web.py index fdd6e7e4..f3a65f51 100644 --- a/test/test_web.py +++ b/test/test_web.py @@ -13,6 +13,7 @@ import urllib.error import http.client from pathlib import Path +from typing import cast, Iterable import aiohttp from selenium import webdriver @@ -28,6 +29,7 @@ from shc.datatypes import RangeFloat1, RGBUInt8, RangeUInt8 from shc.interfaces._helper import ReadableStatusInterface from shc.supervisor import InterfaceStatus, ServiceStatus +from shc.web.widgets import AbstractButton from ._helper import InterfaceThreadRunner, ExampleReadable, async_test @@ -95,9 +97,9 @@ def test_page(self) -> None: self.assertIn('Home Page', self.driver.title) self.assertIn('Another segment', self.driver.page_source) button = self.driver.find_element(By.XPATH, '//button[normalize-space(text()) = "Foobar"]') - self.assertIn("My button group", button.find_element(By.XPATH, '../..').text) + self.assertIn("My button group", button.find_element(By.XPATH, '../../../..').text) button = self.driver.find_element(By.XPATH, '//button[normalize-space(text()) = "Bar"]') - self.assertIn("Another button group", button.find_element(By.XPATH, '../..').text) + self.assertIn("Another button group", button.find_element(By.XPATH, '../../../..').text) def test_main_menu(self) -> None: self.server.page('index', menu_entry="Home", menu_icon='home') @@ -286,7 +288,9 @@ def test_buttons(self) -> None: ExampleReadable(int, 42).connect(b4) page = self.server.page('index') - page.add_item(shc.web.widgets.ButtonGroup("My button group", [b1, b2, b3, b4])) + page.add_item( + shc.web.widgets.ButtonGroup("My button group", cast(Iterable[AbstractButton], [b1, b2, b3, b4])) + ) with unittest.mock.patch.object(b1, '_publish') as b1_publish, \ unittest.mock.patch.object(b3, '_publish') as b3_publish, \ From 920f20702670e85b9006f84b0f76b318b4881e94 Mon Sep 17 00:00:00 2001 From: gruhn Date: Sun, 20 Oct 2024 21:25:04 +0200 Subject: [PATCH 5/7] add: parameter description details --- shc/web/widgets.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shc/web/widgets.py b/shc/web/widgets.py index cb2e6fb2..18feaf93 100644 --- a/shc/web/widgets.py +++ b/shc/web/widgets.py @@ -334,7 +334,9 @@ class ButtonGroup(WebPageItem): `Connectable`. :param label: The label to be shown left of the buttons - :param buttons: List or a List if List of button descriptors + :param buttons: List or a List of Lists of button descriptors. A plain list of button descriptors will be + grouped all together, whereas providing multiple lists each list will be grouped together with a small gap + between each group of button descriptors. """ def __init__( self, From 531087b4d39420823c7124f725b14ef85123cd05 Mon Sep 17 00:00:00 2001 From: gruhn Date: Sun, 20 Oct 2024 21:25:14 +0200 Subject: [PATCH 6/7] add: button group test --- test/test_web.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/test_web.py b/test/test_web.py index f3a65f51..b24245d0 100644 --- a/test/test_web.py +++ b/test/test_web.py @@ -101,6 +101,27 @@ def test_page(self) -> None: button = self.driver.find_element(By.XPATH, '//button[normalize-space(text()) = "Bar"]') self.assertIn("Another button group", button.find_element(By.XPATH, '../../../..').text) + def test_buttongroup_groups(self) -> None: + page = self.server.page('index', 'Home Page') + page.add_item(shc.web.widgets.ButtonGroup("My button group", [ + [shc.web.widgets.StatelessButton(13, "Foo"), + shc.web.widgets.StatelessButton(27, "Bar")], + [shc.web.widgets.StatelessButton(142, "Gaga")], + ])) + + self.server_runner.start() + self.driver.get("http://localhost:42080") + + # buttons in 1st group exist and are grouped + button = self.driver.find_element(By.XPATH, '//button[normalize-space(text()) = "Foo"]') + self.assertEqual("Foo\nBar", button.find_element(By.XPATH, '..').text) + button = self.driver.find_element(By.XPATH, '//button[normalize-space(text()) = "Bar"]') + self.assertEqual("Foo\nBar", button.find_element(By.XPATH, '..').text) + + # button gaga in 2nd group exist and is the only member in the group + button = self.driver.find_element(By.XPATH, '//button[normalize-space(text()) = "Gaga"]') + self.assertEqual("Gaga", button.find_element(By.XPATH, '..').text) + def test_main_menu(self) -> None: self.server.page('index', menu_entry="Home", menu_icon='home') self.server.add_menu_entry('another_page', label="Foo", sub_label="Bar", sub_icon="bars") From f017ccd53c69b55e6d47178cb56ad03c8f4db7c2 Mon Sep 17 00:00:00 2001 From: gruhn Date: Mon, 28 Oct 2024 08:11:37 +0100 Subject: [PATCH 7/7] fix: flake issue --- test/test_web.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_web.py b/test/test_web.py index b24245d0..377c79bc 100644 --- a/test/test_web.py +++ b/test/test_web.py @@ -105,7 +105,7 @@ def test_buttongroup_groups(self) -> None: page = self.server.page('index', 'Home Page') page.add_item(shc.web.widgets.ButtonGroup("My button group", [ [shc.web.widgets.StatelessButton(13, "Foo"), - shc.web.widgets.StatelessButton(27, "Bar")], + shc.web.widgets.StatelessButton(27, "Bar")], [shc.web.widgets.StatelessButton(142, "Gaga")], ]))