diff --git a/app/src/api.js b/app/src/api.js
index e4790a10..444f4110 100644
--- a/app/src/api.js
+++ b/app/src/api.js
@@ -532,7 +532,7 @@ export async function importMapFiles(paths) {
}
}
-export async function clearDownloads() {
+export async function clearCompletedDownloads() {
let response = await apiPost(`${API_URI}/download/clear_completed`);
if (response.status === 204) {
return null
@@ -540,7 +540,21 @@ export async function clearDownloads() {
toast({
type: 'error',
title: 'Error!',
- description: 'Could not clear downloads! See server logs.',
+ description: 'Could not clear completed downloads! See server logs.',
+ time: 5000,
+ });
+ }
+}
+
+export async function clearFailedDownloads() {
+ let response = await apiPost(`${API_URI}/download/clear_failed`);
+ if (response.status === 204) {
+ return null
+ } else {
+ toast({
+ type: 'error',
+ title: 'Error!',
+ description: 'Could not clear failed downloads! See server logs.',
time: 5000,
});
}
diff --git a/app/src/components/Admin.js b/app/src/components/Admin.js
index 9b3b7163..864b4c2b 100644
--- a/app/src/components/Admin.js
+++ b/app/src/components/Admin.js
@@ -11,7 +11,15 @@ import {
Placeholder,
Table
} from "semantic-ui-react";
-import {clearDownloads, getDownloads, getSettings, killDownload, postDownload, saveSettings} from "../api";
+import {
+ clearCompletedDownloads,
+ clearFailedDownloads,
+ getDownloads,
+ getSettings,
+ killDownload,
+ postDownload,
+ saveSettings
+} from "../api";
import TimezoneSelect from 'react-timezone-select';
import {
DisableDownloadsToggle,
@@ -203,8 +211,11 @@ function ClearCompleteDownloads() {
async function localClearDownloads() {
setDisabled(true);
- await clearDownloads();
- setDisabled(false);
+ try {
+ await clearCompletedDownloads();
+ } finally {
+ setDisabled(false);
+ }
}
return <>
@@ -216,6 +227,36 @@ function ClearCompleteDownloads() {
>
}
+function ClearFailedDownloads() {
+ const [open, setOpen] = React.useState(false);
+ const [disabled, setDisabled] = React.useState(false);
+
+ async function localDeleteFailed() {
+ setDisabled(true);
+ try {
+ await clearFailedDownloads();
+ } finally {
+ setDisabled(false);
+ setOpen(false);
+ }
+ }
+
+ return <>
+
+ setOpen(false)}
+ onConfirm={localDeleteFailed}
+ />
+ >
+}
+
class DownloadRow extends React.Component {
render() {
@@ -408,6 +449,7 @@ class Downloads extends React.Component {
onceTable = (
<>
+
{stoppableHeader}
diff --git a/wrolpi/downloader.py b/wrolpi/downloader.py
index a6cce697..c1c03b02 100644
--- a/wrolpi/downloader.py
+++ b/wrolpi/downloader.py
@@ -693,6 +693,12 @@ class DownloadManager:
session.query(Download).filter(Download.status == 'complete').delete()
session.commit()
+ @optional_session
+ def delete_failed(self, session: Session):
+ """Delete any failed download records."""
+ session.query(Download).filter(Download.status == 'failed').delete()
+ session.commit()
+
# The global DownloadManager. This should be used everywhere!
download_manager = DownloadManager()
diff --git a/wrolpi/root_api.py b/wrolpi/root_api.py
index cac684e0..77e93f71 100644
--- a/wrolpi/root_api.py
+++ b/wrolpi/root_api.py
@@ -235,6 +235,13 @@ async def clear_completed(_: Request):
return response.empty()
+@root_api.post('/download/clear_failed')
+@openapi.description('Clear failed downloads')
+async def clear_failed(_: Request):
+ download_manager.delete_failed()
+ return response.empty()
+
+
@root_api.delete('/download/')
@openapi.description('Delete a download')
@wrol_mode_check
diff --git a/wrolpi/test/test_root_api.py b/wrolpi/test/test_root_api.py
index f987fb8c..7f80c8b1 100644
--- a/wrolpi/test/test_root_api.py
+++ b/wrolpi/test/test_root_api.py
@@ -202,7 +202,8 @@ def test_hotspot_settings(test_session, test_client, test_config):
def test_throttle_toggle(test_session, test_client, test_config):
- with mock.patch('wrolpi.admin.subprocess') as mock_subprocess:
+ with mock.patch('wrolpi.admin.subprocess') as mock_subprocess, \
+ mock.patch('wrolpi.admin.CPUFREQ_INFO', "this value isn't even used"):
mock_subprocess.check_output.side_effect = [
b'wlan0: unavailable',
b'The governor "ondemand" may decide ',
@@ -221,7 +222,8 @@ def test_clear_downloads(test_session, test_client, test_config):
d2 = Download(url='https://example.com/2', status='pending')
d3 = Download(url='https://example.com/3', status='deferred')
d4 = Download(url='https://example.com/4', status='new')
- session.add_all([d1, d2, d3, d4])
+ d5 = Download(url='https://example.com/5', status='failed')
+ session.add_all([d1, d2, d3, d4, d5])
def check_download(download_, expected_):
assert download_['url'] == expected_['url']
@@ -230,6 +232,7 @@ def test_clear_downloads(test_session, test_client, test_config):
request, response = api_app.test_client.get('/api/download')
expected = [
{'url': 'https://example.com/2', 'status': 'pending'},
+ {'url': 'https://example.com/5', 'status': 'failed'},
{'url': 'https://example.com/4', 'status': 'new'},
{'url': 'https://example.com/3', 'status': 'deferred'},
{'url': 'https://example.com/1', 'status': 'complete'},
@@ -240,6 +243,19 @@ def test_clear_downloads(test_session, test_client, test_config):
request, response = api_app.test_client.post('/api/download/clear_completed')
assert response.status_code == HTTPStatus.NO_CONTENT
+ request, response = api_app.test_client.get('/api/download')
+ expected = [
+ {'url': 'https://example.com/2', 'status': 'pending'},
+ {'url': 'https://example.com/5', 'status': 'failed'},
+ {'url': 'https://example.com/4', 'status': 'new'},
+ {'url': 'https://example.com/3', 'status': 'deferred'},
+ ]
+ for download, expected in zip_longest(response.json['once_downloads'], expected):
+ check_download(download, expected)
+
+ request, response = api_app.test_client.post('/api/download/clear_failed')
+ assert response.status_code == HTTPStatus.NO_CONTENT
+
request, response = api_app.test_client.get('/api/download')
expected = [
{'url': 'https://example.com/2', 'status': 'pending'},