diff --git a/app/src/api.js b/app/src/api.js
index 611cc71b..e4790a10 100644
--- a/app/src/api.js
+++ b/app/src/api.js
@@ -531,3 +531,17 @@ export async function importMapFiles(paths) {
});
}
}
+
+export async function clearDownloads() {
+ let response = await apiPost(`${API_URI}/download/clear_completed`);
+ if (response.status === 204) {
+ return null
+ } else {
+ toast({
+ type: 'error',
+ title: 'Error!',
+ description: 'Could not clear downloads! See server logs.',
+ time: 5000,
+ });
+ }
+}
diff --git a/app/src/components/Admin.js b/app/src/components/Admin.js
index 2aa52fbc..9b3b7163 100644
--- a/app/src/components/Admin.js
+++ b/app/src/components/Admin.js
@@ -11,7 +11,7 @@ import {
Placeholder,
Table
} from "semantic-ui-react";
-import {getDownloads, getSettings, killDownload, postDownload, saveSettings} from "../api";
+import {clearDownloads, getDownloads, getSettings, killDownload, postDownload, saveSettings} from "../api";
import TimezoneSelect from 'react-timezone-select';
import {
DisableDownloadsToggle,
@@ -198,6 +198,24 @@ class WROLMode extends React.Component {
}
}
+function ClearCompleteDownloads() {
+ const [disabled, setDisabled] = React.useState(false);
+
+ async function localClearDownloads() {
+ setDisabled(true);
+ await clearDownloads();
+ setDisabled(false);
+ }
+
+ return <>
+
+ >
+}
+
class DownloadRow extends React.Component {
render() {
@@ -387,14 +405,18 @@ class Downloads extends React.Component {
if (this.state.once_downloads !== null && this.state.once_downloads.length === 0) {
onceTable =
No downloads are scheduled to be downloaded.
} else if (this.state.once_downloads !== null) {
- onceTable = (
- {stoppableHeader}
-
- {this.state.once_downloads.map((i) =>
-
- )}
-
-
);
+ onceTable = (
+ <>
+
+
+ {stoppableHeader}
+
+ {this.state.once_downloads.map((i) =>
+
+ )}
+
+
+ >);
}
let recurringTable = tablePlaceholder;
diff --git a/wrolpi/admin.py b/wrolpi/admin.py
index cd857468..cbff952a 100644
--- a/wrolpi/admin.py
+++ b/wrolpi/admin.py
@@ -23,6 +23,9 @@ class HotspotStatus(enum.Enum):
def hotspot_status() -> HotspotStatus:
+ if NMCLI is None:
+ return HotspotStatus.unknown
+
cmd = (NMCLI,)
try:
output = subprocess.check_output(cmd).decode().strip()
@@ -97,6 +100,9 @@ GOVERNOR_MAP = {
def throttle_status() -> GovernorStatus:
+ if not CPUFREQ_INFO:
+ return GovernorStatus.unknown
+
cmd = (CPUFREQ_INFO,)
try:
output = subprocess.check_output(cmd)
diff --git a/wrolpi/downloader.py b/wrolpi/downloader.py
index e58cdeaf..a6cce697 100644
--- a/wrolpi/downloader.py
+++ b/wrolpi/downloader.py
@@ -687,6 +687,12 @@ class DownloadManager:
next_download = local_timezone(next_download)
return next_download
+ @optional_session
+ def delete_completed(self, session: Session):
+ """Delete any completed download records."""
+ session.query(Download).filter(Download.status == 'complete').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 a898cb94..cac684e0 100644
--- a/wrolpi/root_api.py
+++ b/wrolpi/root_api.py
@@ -228,6 +228,13 @@ async def enable_downloads(_: Request):
return response.empty()
+@root_api.post('/download/clear_completed')
+@openapi.description('Clear completed downloads')
+async def clear_completed(_: Request):
+ download_manager.delete_completed()
+ 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 144dba72..f987fb8c 100644
--- a/wrolpi/test/test_root_api.py
+++ b/wrolpi/test/test_root_api.py
@@ -213,3 +213,38 @@ def test_throttle_toggle(test_session, test_client, test_config):
assert response.status_code == HTTPStatus.OK
assert response.json['throttle_on_startup'] is False
assert response.json['throttle_status'] == 'ondemand'
+
+
+def test_clear_downloads(test_session, test_client, test_config):
+ with get_db_session(commit=True) as session:
+ d1 = Download(url='https://example.com/1', status='complete')
+ 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])
+
+ def check_download(download_, expected_):
+ assert download_['url'] == expected_['url']
+ assert download_['status'] == expected_['status']
+
+ request, response = api_app.test_client.get('/api/download')
+ expected = [
+ {'url': 'https://example.com/2', 'status': 'pending'},
+ {'url': 'https://example.com/4', 'status': 'new'},
+ {'url': 'https://example.com/3', 'status': 'deferred'},
+ {'url': 'https://example.com/1', 'status': 'complete'},
+ ]
+ 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_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/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)