diff --git a/app/src/components/Archive.js b/app/src/components/Archive.js
index bd1eeee4..8867d5a0 100644
--- a/app/src/components/Archive.js
+++ b/app/src/components/Archive.js
@@ -1,6 +1,6 @@
import React, {useState} from "react";
import {Card, Confirm, Container, Icon, Image, Placeholder, Tab} from "semantic-ui-react";
-import Paginator, {ClearButton, ExternalLink, SearchInput, textEllipsis, truncateLong, uploadDate} from "./Common";
+import Paginator, {ClearButton, ExternalLink, SearchInput, textEllipsis, uploadDate} from "./Common";
import {deleteArchive, postDownload, refreshArchives} from "../api";
import Button from "semantic-ui-react/dist/commonjs/elements/Button";
import {Link, NavLink} from "react-router-dom";
@@ -8,6 +8,7 @@ import {ArchivePlaceholder} from "./Placeholder";
import Table from "semantic-ui-react/dist/commonjs/collections/Table";
import {useArchives} from "../hooks/useArchives";
import {useDomains} from "../hooks/useDomains";
+import Message from "semantic-ui-react/dist/commonjs/collections/Message";
function FailedArchiveCard({archive, syncArchive, deleteArchive}) {
@@ -163,7 +164,10 @@ export function ArchivesList({archives, searchStr, syncArchive, localDeleteArchi
return
No archives found! Is your search too restrictive?
;
} else if (archives.length === 0) {
// No archives fetched.
- return No archives found! Have you archived any webpages?
;
+ return
+ No archives
+ Have you archived any webpages?
+ ;
} else {
// Archives fetched successfully!
return ;
@@ -245,14 +249,14 @@ function Domains() {
}
- if (domains) {
+ if (domains && domains.length > 0) {
return (
<>
Domain
- URLs
+ Archives
@@ -264,13 +268,22 @@ function Domains() {
)
}
+ if (domains === null) {
+ return (<>
+
+
+
+
+
+
+ >)
+ }
+
return (<>
-
-
-
-
-
-
+
+ No domains yet.
+ Archive some webpages!
+
>)
}
diff --git a/modules/inventory/api.py b/modules/inventory/api.py
index dd25d89d..cc605c09 100644
--- a/modules/inventory/api.py
+++ b/modules/inventory/api.py
@@ -117,13 +117,17 @@ def put_item(_: Request, item_id: int, data: dict):
@bp.delete('/item/')
+@bp.delete('/item/')
@validate_doc(
'Delete items from an inventory.',
)
@run_after(save_inventories_file)
def item_delete(_: Request, item_ids: str):
try:
- item_ids = [int(i) for i in item_ids.split(',')]
+ if isinstance(item_ids, int):
+ item_ids = [item_ids, ]
+ else:
+ item_ids = [int(i) for i in item_ids.split(',')]
except ValueError:
raise ValidationError('Could not parse item_ids')
diff --git a/modules/inventory/conftest.py b/modules/inventory/conftest.py
new file mode 100644
index 00000000..d6cc0ab4
--- /dev/null
+++ b/modules/inventory/conftest.py
@@ -0,0 +1,26 @@
+import pytest
+
+from modules.inventory import Inventory, Item, DEFAULT_CATEGORIES, DEFAULT_INVENTORIES
+
+
+@pytest.fixture
+def test_inventory(test_session):
+ inventory = Inventory(name='Test Inventory')
+ test_session.add(inventory)
+ test_session.commit()
+ return inventory
+
+
+@pytest.fixture
+def init_test_inventory(test_session):
+ for subcategory, category in DEFAULT_CATEGORIES:
+ item = Item(subcategory=subcategory, category=category)
+ test_session.add(item)
+
+ for name in DEFAULT_INVENTORIES:
+ inv = Inventory(name=name)
+ test_session.add(inv)
+
+ inventory = test_session.query(Inventory).filter_by(name='Food Storage').one()
+ test_session.commit()
+ return inventory
diff --git a/modules/inventory/inventory.py b/modules/inventory/inventory.py
index ccf80f84..cbd8b05b 100644
--- a/modules/inventory/inventory.py
+++ b/modules/inventory/inventory.py
@@ -10,6 +10,7 @@ from sqlalchemy.orm.exc import NoResultFound
from wrolpi.common import logger, Base
from wrolpi.db import get_db_session, get_db_curs
+from wrolpi.errors import APIError
from .models import Inventory, Item, InventoriesVersion
logger = logger.getChild(__name__)
@@ -174,7 +175,10 @@ def update_item(item_id: int, item: dict):
def delete_items(items_ids: List[int]):
with get_db_curs(commit=True) as curs:
- curs.execute('UPDATE item SET deleted_at=current_timestamp WHERE id = ANY(%s)', (items_ids,))
+ curs.execute('UPDATE item SET deleted_at=current_timestamp WHERE id = ANY(%s) RETURNING id', (items_ids,))
+ deleted_ids = {i['id'] for i in curs.fetchall()}
+ if set(items_ids) != deleted_ids:
+ raise APIError('Could not delete the items')
def get_inventories_version():
diff --git a/modules/inventory/test/test_api.py b/modules/inventory/test/test_api.py
new file mode 100644
index 00000000..e686f039
--- /dev/null
+++ b/modules/inventory/test/test_api.py
@@ -0,0 +1,51 @@
+from http import HTTPStatus
+
+from modules.inventory import Item
+from modules.inventory.inventory import save_item
+
+
+def test_delete_items(test_session, init_test_inventory, test_client):
+ """
+ Multiple Items can be deleted in a single request.
+ """
+ item = dict(
+ brand='Ocean',
+ name='Salt',
+ count='8.0',
+ unit='lbs',
+ item_size='25.0',
+ category='cooking ingredients',
+ subcategory='salt',
+ )
+ save_item(init_test_inventory.id, item)
+ save_item(init_test_inventory.id, item)
+ items = test_session.query(Item).filter(Item.brand != None).all()
+
+ request, response = test_client.delete(f'/api/inventory/item/{items[0].id},{items[1].id}')
+ assert response.status_code == HTTPStatus.NO_CONTENT, response.status_code
+ # Refresh all items. Verify they were deleted.
+ [test_session.refresh(i) for i in items]
+ assert all([i.deleted_at for i in items])
+
+
+def test_delete_item(test_session, init_test_inventory, test_client):
+ """
+ An item can be deleted by itself.
+ """
+ item = dict(
+ brand='Ocean',
+ name='Salt',
+ count='8.0',
+ unit='lbs',
+ item_size='25.0',
+ category='cooking ingredients',
+ subcategory='salt',
+ )
+ save_item(init_test_inventory.id, item)
+ item = test_session.query(Item).filter(Item.brand != None).one()
+ assert not item.deleted_at
+
+ request, response = test_client.delete(f'/api/inventory/item/{item.id}')
+ assert response.status_code == HTTPStatus.NO_CONTENT, response.status_code
+ test_session.refresh(item)
+ assert item.deleted_at
diff --git a/modules/inventory/test/test_inventory.py b/modules/inventory/test/test_inventory.py
index c5011e49..9da58ceb 100644
--- a/modules/inventory/test/test_inventory.py
+++ b/modules/inventory/test/test_inventory.py
@@ -277,7 +277,7 @@ def quantity_to_string(quantity: Quantity) -> str:
]
)
def test_compact_unit(quantity, expected):
- # Round the result so we don't have to specify all those zeros for the test definition.
+ # Round the result, so we don't have to specify all those zeros for the test definition.
assert quantity_to_string(compact_unit(quantity)) == expected
@@ -296,7 +296,7 @@ length = gallon.dimensionality
{(('grains',), oz.dimensionality): Decimal('1') * oz},
),
(
- # The larger of the units is what is returned.
+ # The largest of the units is what is returned.
[
{'category': 'grains', 'count': Decimal('1'), 'item_size': Decimal('1'), 'unit': 'oz'},
{'category': 'grains', 'count': Decimal('1'), 'item_size': Decimal('1'), 'unit': 'lbs'},
diff --git a/modules/videos/__init__.py b/modules/videos/__init__.py
index 8bfe14d6..5cd95bbb 100644
--- a/modules/videos/__init__.py
+++ b/modules/videos/__init__.py
@@ -1 +1,8 @@
from modules.videos import downloader
+from wrolpi import before_startup
+
+
+@before_startup
+def startup_spread_channel_downloads():
+ from .channel import lib as channel_lib
+ channel_lib.spread_channel_downloads()
diff --git a/modules/videos/api.py b/modules/videos/api.py
index 82af9f0b..719a30c1 100644
--- a/modules/videos/api.py
+++ b/modules/videos/api.py
@@ -5,7 +5,6 @@ from http import HTTPStatus
from sanic import Blueprint, response
from sanic.request import Request
-from wrolpi import before_startup
from wrolpi.common import create_websocket_feed, get_sanic_url, \
wrol_mode_check
from wrolpi.common import logger
@@ -128,8 +127,3 @@ async def favorite(_: Request, data: dict):
async def statistics(_: Request):
ret = await get_statistics()
return json_response(ret, HTTPStatus.OK)
-
-
-@before_startup
-def startup_spread_channel_downloads():
- channel_lib.spread_channel_downloads()