Skip to content

Commit

Permalink
Persist shopping list + clear completed (#8697)
Browse files Browse the repository at this point in the history
  • Loading branch information
balloob committed Jul 29, 2017
1 parent d1b73a9 commit 0bde0a6
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 13 deletions.
71 changes: 61 additions & 10 deletions homeassistant/components/shopping_list.py
@@ -1,6 +1,8 @@
"""Component to manage a shoppling list."""
import asyncio
import json
import logging
import os
import uuid

import voluptuous as vol
Expand All @@ -23,43 +25,55 @@
'complete': bool,
'name': str,
})
PERSISTENCE = '.shopping_list.json'


@asyncio.coroutine
def async_setup(hass, config):
"""Initialize the shopping list."""
hass.data[DOMAIN] = ShoppingData([])
data = hass.data[DOMAIN] = ShoppingData(hass)
yield from data.async_load()

intent.async_register(hass, AddItemIntent())
intent.async_register(hass, ListTopItemsIntent())

hass.http.register_view(ShoppingListView)
hass.http.register_view(UpdateShoppingListItemView)
hass.http.register_view(ClearCompletedItemsView)

hass.components.conversation.async_register(INTENT_ADD_ITEM, [
'Add {item} to my shopping list',
])
hass.components.conversation.async_register(INTENT_LAST_ITEMS, [
'What is on my shopping list'
])

hass.components.frontend.register_built_in_panel(
'shopping-list', 'Shopping List', 'mdi:cart')

return True


class ShoppingData:
"""Class to hold shopping list data."""

def __init__(self, items):
def __init__(self, hass):
"""Initialize the shopping list."""
self.items = items
self.hass = hass
self.items = []

def add(self, name):
@callback
def async_add(self, name):
"""Add a shopping list item."""
self.items.append({
'name': name,
'id': uuid.uuid4().hex,
'complete': False
})
self.hass.async_add_job(self.save)

def update(self, item_id, info):
@callback
def async_update(self, item_id, info):
"""Update a shopping list item."""
item = next((itm for itm in self.items if itm['id'] == item_id), None)

Expand All @@ -68,11 +82,33 @@ def update(self, item_id, info):

info = ITEM_UPDATE_SCHEMA(info)
item.update(info)
self.hass.async_add_job(self.save)
return item

def clear_completed(self):
@callback
def async_clear_completed(self):
"""Clear completed items."""
self.items = [itm for itm in self.items if not itm['complete']]
self.hass.async_add_job(self.save)

@asyncio.coroutine
def async_load(self):
"""Load items."""
def load():
"""Load the items synchronously."""
path = self.hass.config.path(PERSISTENCE)
if not os.path.isfile(path):
return []
with open(path) as file:
return json.loads(file.read())

items = yield from self.hass.async_add_job(load)
self.items = items

def save(self):
"""Save the items."""
with open(self.hass.config.path(PERSISTENCE), 'wt') as file:
file.write(json.dumps(self.items, sort_keys=True, indent=4))


class AddItemIntent(intent.IntentHandler):
Expand All @@ -88,7 +124,7 @@ def async_handle(self, intent_obj):
"""Handle the intent."""
slots = self.async_validate_slots(intent_obj.slots)
item = slots['item']['value']
intent_obj.hass.data[DOMAIN].add(item)
intent_obj.hass.data[DOMAIN].async_add(item)

response = intent_obj.create_response()
response.async_set_speech(
Expand Down Expand Up @@ -137,19 +173,34 @@ def get(self, request):
class UpdateShoppingListItemView(http.HomeAssistantView):
"""View to retrieve shopping list content."""

url = '/api/shopping_list/{item_id}'
name = "api:shopping_list:id"
url = '/api/shopping_list/item/{item_id}'
name = "api:shopping_list:item:id"

@callback
def post(self, request, item_id):
"""Retrieve if API is running."""
data = yield from request.json()

try:
item = request.app['hass'].data[DOMAIN].update(item_id, data)
item = request.app['hass'].data[DOMAIN].async_update(item_id, data)
request.app['hass'].bus.async_fire(EVENT)
return self.json(item)
except KeyError:
return self.json_message('Item not found', HTTP_NOT_FOUND)
except vol.Invalid:
return self.json_message('Item not found', HTTP_BAD_REQUEST)


class ClearCompletedItemsView(http.HomeAssistantView):
"""View to retrieve shopping list content."""

url = '/api/shopping_list/clear_completed'
name = "api:shopping_list:clear_completed"

@callback
def post(self, request):
"""Retrieve if API is running."""
hass = request.app['hass']
hass.data[DOMAIN].async_clear_completed()
hass.bus.async_fire(EVENT)
return self.json_message('Cleared completed items.')
53 changes: 50 additions & 3 deletions tests/components/test_shopping_list.py
@@ -1,10 +1,20 @@
"""Test shopping list component."""
import asyncio
from unittest.mock import patch

import pytest

from homeassistant.bootstrap import async_setup_component
from homeassistant.helpers import intent


@pytest.fixture(autouse=True)
def mock_shopping_list_save():
"""Stub out the persistence."""
with patch('homeassistant.components.shopping_list.ShoppingData.save'):
yield


@asyncio.coroutine
def test_add_item(hass):
"""Test adding an item intent."""
Expand Down Expand Up @@ -82,7 +92,7 @@ def test_api_update(hass, test_client):

client = yield from test_client(hass.http.app)
resp = yield from client.post(
'/api/shopping_list/{}'.format(beer_id), json={
'/api/shopping_list/item/{}'.format(beer_id), json={
'name': 'soda'
})

Expand All @@ -95,7 +105,7 @@ def test_api_update(hass, test_client):
}

resp = yield from client.post(
'/api/shopping_list/{}'.format(wine_id), json={
'/api/shopping_list/item/{}'.format(wine_id), json={
'complete': True
})

Expand Down Expand Up @@ -140,8 +150,45 @@ def test_api_update_fails(hass, test_client):
beer_id = hass.data['shopping_list'].items[0]['id']
client = yield from test_client(hass.http.app)
resp = yield from client.post(
'/api/shopping_list/{}'.format(beer_id), json={
'/api/shopping_list/item/{}'.format(beer_id), json={
'name': 123,
})

assert resp.status == 400


@asyncio.coroutine
def test_api_clear_completed(hass, test_client):
"""Test the API."""
yield from async_setup_component(hass, 'shopping_list', {})

yield from intent.async_handle(
hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'beer'}}
)
yield from intent.async_handle(
hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'wine'}}
)

beer_id = hass.data['shopping_list'].items[0]['id']
wine_id = hass.data['shopping_list'].items[1]['id']

client = yield from test_client(hass.http.app)

# Mark beer as completed
resp = yield from client.post(
'/api/shopping_list/item/{}'.format(beer_id), json={
'complete': True
})
assert resp.status == 200

resp = yield from client.post('/api/shopping_list/clear_completed')
assert resp.status == 200

items = hass.data['shopping_list'].items
assert len(items) == 1

assert items[0] == {
'id': wine_id,
'name': 'wine',
'complete': False
}

0 comments on commit 0bde0a6

Please sign in to comment.