diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index 8618e9bafb7..1831f894cec 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -128,6 +128,8 @@ async def async_setup_entry(hass, config_entry): SCHEMA_WEBSOCKET_CLEAR_ITEMS, ) + websocket_api.async_register_command(hass, websocket_handle_reorder) + return True @@ -163,6 +165,31 @@ class ShoppingData: self.items = [itm for itm in self.items if not itm["complete"]] await self.hass.async_add_executor_job(self.save) + @callback + def async_reorder(self, item_ids): + """Reorder items.""" + # The array for sorted items. + new_items = [] + all_items_mapping = {item["id"]: item for item in self.items} + # Append items by the order of passed in array. + for item_id in item_ids: + if item_id not in all_items_mapping: + raise KeyError + new_items.append(all_items_mapping[item_id]) + # Remove the item from mapping after it's appended in the result array. + del all_items_mapping[item_id] + # Append the rest of the items + for key in all_items_mapping: + # All the unchecked items must be passed in the item_ids array, + # so all items left in the mapping should be checked items. + if all_items_mapping[key]["complete"] is False: + raise vol.Invalid( + "The item ids array doesn't contain all the unchecked shopping list items." + ) + new_items.append(all_items_mapping[key]) + self.items = new_items + self.hass.async_add_executor_job(self.save) + async def async_load(self): """Load items.""" @@ -277,3 +304,26 @@ async def websocket_handle_clear(hass, connection, msg): await hass.data[DOMAIN].async_clear_completed() hass.bus.async_fire(EVENT, {"action": "clear"}) connection.send_message(websocket_api.result_message(msg["id"])) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "shopping_list/items/reorder", + vol.Required("item_ids"): [str], + } +) +def websocket_handle_reorder(hass, connection, msg): + """Handle reordering shopping_list items.""" + msg_id = msg.pop("id") + try: + hass.data[DOMAIN].async_reorder(msg.pop("item_ids")) + hass.bus.async_fire(EVENT, {"action": "reorder"}) + connection.send_result(msg_id) + except KeyError: + connection.send_error( + msg_id, + websocket_api.const.ERR_NOT_FOUND, + "One or more item id(s) not found.", + ) + except vol.Invalid as err: + connection.send_error(msg_id, websocket_api.const.ERR_INVALID_FORMAT, f"{err}") diff --git a/tests/components/shopping_list/test_init.py b/tests/components/shopping_list/test_init.py index b8930114665..0be4c70ef18 100644 --- a/tests/components/shopping_list/test_init.py +++ b/tests/components/shopping_list/test_init.py @@ -1,6 +1,10 @@ """Test shopping list component.""" -from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.components.websocket_api.const import ( + ERR_INVALID_FORMAT, + ERR_NOT_FOUND, + TYPE_RESULT, +) from homeassistant.const import HTTP_NOT_FOUND from homeassistant.helpers import intent @@ -311,3 +315,125 @@ async def test_ws_add_item_fail(hass, hass_ws_client, sl_setup): msg = await client.receive_json() assert msg["success"] is False assert len(hass.data["shopping_list"].items) == 0 + + +async def test_ws_reorder_items(hass, hass_ws_client, sl_setup): + """Test reordering shopping_list items websocket command.""" + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} + ) + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "wine"}} + ) + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "apple"}} + ) + + beer_id = hass.data["shopping_list"].items[0]["id"] + wine_id = hass.data["shopping_list"].items[1]["id"] + apple_id = hass.data["shopping_list"].items[2]["id"] + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 6, + "type": "shopping_list/items/reorder", + "item_ids": [wine_id, apple_id, beer_id], + } + ) + msg = await client.receive_json() + assert msg["success"] is True + assert hass.data["shopping_list"].items[0] == { + "id": wine_id, + "name": "wine", + "complete": False, + } + assert hass.data["shopping_list"].items[1] == { + "id": apple_id, + "name": "apple", + "complete": False, + } + assert hass.data["shopping_list"].items[2] == { + "id": beer_id, + "name": "beer", + "complete": False, + } + + # Mark wine as completed. + await client.send_json( + { + "id": 7, + "type": "shopping_list/items/update", + "item_id": wine_id, + "complete": True, + } + ) + _ = await client.receive_json() + + await client.send_json( + { + "id": 8, + "type": "shopping_list/items/reorder", + "item_ids": [apple_id, beer_id], + } + ) + msg = await client.receive_json() + assert msg["success"] is True + assert hass.data["shopping_list"].items[0] == { + "id": apple_id, + "name": "apple", + "complete": False, + } + assert hass.data["shopping_list"].items[1] == { + "id": beer_id, + "name": "beer", + "complete": False, + } + assert hass.data["shopping_list"].items[2] == { + "id": wine_id, + "name": "wine", + "complete": True, + } + + +async def test_ws_reorder_items_failure(hass, hass_ws_client, sl_setup): + """Test reordering shopping_list items websocket command.""" + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} + ) + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "wine"}} + ) + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "apple"}} + ) + + beer_id = hass.data["shopping_list"].items[0]["id"] + wine_id = hass.data["shopping_list"].items[1]["id"] + apple_id = hass.data["shopping_list"].items[2]["id"] + + client = await hass_ws_client(hass) + + # Testing sending bad item id. + await client.send_json( + { + "id": 8, + "type": "shopping_list/items/reorder", + "item_ids": [wine_id, apple_id, beer_id, "BAD_ID"], + } + ) + msg = await client.receive_json() + assert msg["success"] is False + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Testing not sending all unchecked item ids. + await client.send_json( + { + "id": 9, + "type": "shopping_list/items/reorder", + "item_ids": [wine_id, apple_id], + } + ) + msg = await client.receive_json() + assert msg["success"] is False + assert msg["error"]["code"] == ERR_INVALID_FORMAT