mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-07-13 05:55:46 +00:00
feat: add autorefresh button to the main page (#149)
* also add configuration options and change behaviour accordingly
This commit is contained in:
@ -166,6 +166,7 @@ Reporting to web service related settings. In most cases there is fallback to we
|
|||||||
Web server settings. This feature requires ``aiohttp`` libraries to be installed.
|
Web server settings. This feature requires ``aiohttp`` libraries to be installed.
|
||||||
|
|
||||||
* ``address`` - optional address in form ``proto://host:port`` (``port`` can be omitted in case of default ``proto`` ports), will be used instead of ``http://{host}:{port}`` in case if set, string, optional. This option is required in case if ``OAuth`` provider is used.
|
* ``address`` - optional address in form ``proto://host:port`` (``port`` can be omitted in case of default ``proto`` ports), will be used instead of ``http://{host}:{port}`` in case if set, string, optional. This option is required in case if ``OAuth`` provider is used.
|
||||||
|
* ``autorefresh_intervals`` - enable page auto refresh options, space separated list of integers, optional. The first defined interval will be used as default. If no intervals set, the auto refresh buttons will be disabled.
|
||||||
* ``enable_archive_upload`` - allow to upload packages via HTTP (i.e. call of ``/api/v1/service/upload`` uri), boolean, optional, default ``no``.
|
* ``enable_archive_upload`` - allow to upload packages via HTTP (i.e. call of ``/api/v1/service/upload`` uri), boolean, optional, default ``no``.
|
||||||
* ``host`` - host to bind, string, optional.
|
* ``host`` - host to bind, string, optional.
|
||||||
* ``index_url`` - full URL of the repository index page, string, optional.
|
* ``index_url`` - full URL of the repository index page, string, optional.
|
||||||
|
@ -28,6 +28,9 @@ allow_read_only = yes
|
|||||||
; External address of the web service. Will be used for some features like OAuth. If none set will be generated as
|
; External address of the web service. Will be used for some features like OAuth. If none set will be generated as
|
||||||
; address = http://${web:host}:${web:port}
|
; address = http://${web:host}:${web:port}
|
||||||
;address = http://${web:host}:${web:port}
|
;address = http://${web:host}:${web:port}
|
||||||
|
; Enable page auto refresh. Intervals are given in seconds. Default interval is always the first element of the list.
|
||||||
|
; If no intervals set, auto refresh will be disabled.
|
||||||
|
autorefresh_intervals = 5 1 10 30 60
|
||||||
; Enable file upload endpoint used by some triggers.
|
; Enable file upload endpoint used by some triggers.
|
||||||
;enable_archive_upload = no
|
;enable_archive_upload = no
|
||||||
; Address to bind the server.
|
; Address to bind the server.
|
||||||
|
@ -80,6 +80,21 @@
|
|||||||
<button type="button" class="btn btn-secondary" onclick="reload()">
|
<button type="button" class="btn btn-secondary" onclick="reload()">
|
||||||
<i class="bi bi-arrow-clockwise"></i><span class="d-none d-sm-inline"> reload</span>
|
<i class="bi bi-arrow-clockwise"></i><span class="d-none d-sm-inline"> reload</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{% if autorefresh_intervals %}
|
||||||
|
<div class="btn-group">
|
||||||
|
<input id="table-autoreload-button" type="checkbox" class="btn-check" autocomplete="off" onclick="toggleTableAutoReload()" checked>
|
||||||
|
<label for="table-autoreload-button" class="btn btn-outline-secondary" title="toggle auto reload"><i class="bi bi-clock"></i></label>
|
||||||
|
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<span class="visually-hidden">select interval</span>
|
||||||
|
</button>
|
||||||
|
<ul id="table-autoreload-input" class="dropdown-menu">
|
||||||
|
{% for interval in autorefresh_intervals %}
|
||||||
|
<li><a class="dropdown-item {{ "active" if interval.is_active }}" onclick="toggleTableAutoReload({{ interval.interval }})" data-interval="{{ interval.interval }}">{{ interval.text }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table id="packages"
|
<table id="packages"
|
||||||
|
@ -101,9 +101,21 @@
|
|||||||
<button id="package-info-update-button" type="submit" class="btn btn-success" onclick="packageInfoUpdate()" data-bs-dismiss="modal"><i class="bi bi-play"></i><span class="d-none d-sm-inline"> update</span></button>
|
<button id="package-info-update-button" type="submit" class="btn btn-success" onclick="packageInfoUpdate()" data-bs-dismiss="modal"><i class="bi bi-play"></i><span class="d-none d-sm-inline"> update</span></button>
|
||||||
<button id="package-info-remove-button" type="submit" class="btn btn-danger" onclick="packageInfoRemove()" data-bs-dismiss="modal"><i class="bi bi-trash"></i><span class="d-none d-sm-inline"> remove</span></button>
|
<button id="package-info-remove-button" type="submit" class="btn btn-danger" onclick="packageInfoRemove()" data-bs-dismiss="modal"><i class="bi bi-trash"></i><span class="d-none d-sm-inline"> remove</span></button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<input id="package-info-autoreload-button" type="checkbox" class="btn-check" autocomplete="off" onclick="togglePackageInfoAutoReload()" checked>
|
{% if autorefresh_intervals %}
|
||||||
<label for="package-info-autoreload-button" class="btn btn-outline-secondary" title="toggle auto reload"><i class="bi bi-clock"></i></label>
|
<button type="button" class="btn btn-secondary" onclick="showPackageInfo()"><i class="bi bi-arrow-clockwise"></i><span class="d-none d-sm-inline"> reload</span></button>
|
||||||
<button type="button" class="btn btn-secondary" onclick="showPackageInfo()"><i class="bi bi-arrow-clockwise"></i><span class="d-none d-sm-inline"> reload</span></button>
|
<div class="btn-group dropup">
|
||||||
|
<input id="package-info-autoreload-button" type="checkbox" class="btn-check" autocomplete="off" onclick="togglePackageInfoAutoReload()" checked>
|
||||||
|
<label for="package-info-autoreload-button" class="btn btn-outline-secondary" title="toggle auto reload"><i class="bi bi-clock"></i></label>
|
||||||
|
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<span class="visually-hidden">select interval</span>
|
||||||
|
</button>
|
||||||
|
<ul id="package-info-autoreload-input" class="dropdown-menu">
|
||||||
|
{% for interval in autorefresh_intervals %}
|
||||||
|
<li><a class="dropdown-item {{ "active" if interval.is_active }}" onclick="togglePackageInfoAutoReload({{ interval.interval }})" data-interval="{{ interval.interval }}">{{ interval.text }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i><span class="d-none d-sm-inline"> close</span></button>
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i><span class="d-none d-sm-inline"> close</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -143,6 +155,7 @@
|
|||||||
const packageInfoRefreshInput = document.getElementById("package-info-refresh-input");
|
const packageInfoRefreshInput = document.getElementById("package-info-refresh-input");
|
||||||
|
|
||||||
const packageInfoAutoReloadButton = document.getElementById("package-info-autoreload-button");
|
const packageInfoAutoReloadButton = document.getElementById("package-info-autoreload-button");
|
||||||
|
const packageInfoAutoReloadInput = document.getElementById("package-info-autoreload-input");
|
||||||
let packageInfoAutoReloadTask = null;
|
let packageInfoAutoReloadTask = null;
|
||||||
|
|
||||||
function clearChart() {
|
function clearChart() {
|
||||||
@ -477,22 +490,22 @@
|
|||||||
|
|
||||||
if (isPackageBaseSet) {
|
if (isPackageBaseSet) {
|
||||||
bootstrap.Modal.getOrCreateInstance(packageInfoModal).show();
|
bootstrap.Modal.getOrCreateInstance(packageInfoModal).show();
|
||||||
togglePackageInfoAutoReload();
|
{% if autorefresh_intervals %}
|
||||||
|
togglePackageInfoAutoReload();
|
||||||
|
{% endif %}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function togglePackageInfoAutoReload() {
|
function togglePackageInfoAutoReload(interval) {
|
||||||
clearInterval(packageInfoAutoReloadTask);
|
clearInterval(packageInfoAutoReloadTask);
|
||||||
if (packageInfoAutoReloadButton.checked) {
|
packageInfoAutoReloadTask = toggleAutoReload(packageInfoAutoReloadButton, interval, packageInfoAutoReloadInput, _ => {
|
||||||
packageInfoAutoReloadTask = setInterval(_ => {
|
if (!hasActiveSelection()) {
|
||||||
if (!hasActiveSelection()) {
|
const packageBase = packageInfoModal.dataset.package;
|
||||||
const packageBase = packageInfoModal.dataset.package;
|
// we only poll status and logs here
|
||||||
// we only poll status and logs here
|
loadPackage(packageBase);
|
||||||
loadPackage(packageBase);
|
reloadActiveLogs(packageBase);
|
||||||
reloadActiveLogs(packageBase);
|
}
|
||||||
}
|
});
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ready(_ => {
|
ready(_ => {
|
||||||
|
@ -10,6 +10,10 @@
|
|||||||
const dashboardButton = document.getElementById("dashboard-button");
|
const dashboardButton = document.getElementById("dashboard-button");
|
||||||
const versionBadge = document.getElementById("badge-version");
|
const versionBadge = document.getElementById("badge-version");
|
||||||
|
|
||||||
|
const tableAutoReloadButton = document.getElementById("table-autoreload-button");
|
||||||
|
const tableAutoReloadInput = document.getElementById("table-autoreload-input");
|
||||||
|
let tableAutoReloadTask = null;
|
||||||
|
|
||||||
function doPackageAction(uri, packages, repository, successText, failureText, data) {
|
function doPackageAction(uri, packages, repository, successText, failureText, data) {
|
||||||
makeRequest(
|
makeRequest(
|
||||||
uri,
|
uri,
|
||||||
@ -86,8 +90,10 @@
|
|||||||
doPackageAction("/api/v1/service/update", [], repository, onSuccess, onFailure, parameters);
|
doPackageAction("/api/v1/service/update", [], repository, onSuccess, onFailure, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reload() {
|
function reload(silent) {
|
||||||
table.bootstrapTable("showLoading");
|
if (!silent) {
|
||||||
|
table.bootstrapTable("showLoading");
|
||||||
|
}
|
||||||
|
|
||||||
const badgeClass = status => {
|
const badgeClass = status => {
|
||||||
if (status === "pending") return "btn-outline-warning";
|
if (status === "pending") return "btn-outline-warning";
|
||||||
@ -128,16 +134,18 @@
|
|||||||
table.bootstrapTable("hideLoading");
|
table.bootstrapTable("hideLoading");
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
if ((error.status === 401) || (error.status === 403)) {
|
if (!silent) {
|
||||||
// authorization error
|
if ((error.status === 401) || (error.status === 403)) {
|
||||||
const text = "In order to see statuses you must login first.";
|
// authorization error
|
||||||
table.find("tr.unauthorized").remove();
|
const text = "In order to see statuses you must login first.";
|
||||||
table.find("tbody").append(`<tr class="unauthorized"><td colspan="100%">${safe(text)}</td></tr>`);
|
table.find("tr.unauthorized").remove();
|
||||||
table.bootstrapTable("hideLoading");
|
table.find("tbody").append(`<tr class="unauthorized"><td colspan="100%">${safe(text)}</td></tr>`);
|
||||||
} else {
|
table.bootstrapTable("hideLoading");
|
||||||
// other errors
|
} else {
|
||||||
const message = details => `Could not load list of packages: ${details}`;
|
// other errors
|
||||||
showFailure("Load failure", message, error);
|
const message = details => `Could not load list of packages: ${details}`;
|
||||||
|
showFailure("Load failure", message, error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -230,6 +238,15 @@
|
|||||||
return {classes: cellClass(value)};
|
return {classes: cellClass(value)};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleTableAutoReload(interval) {
|
||||||
|
clearInterval(tableAutoReloadTask);
|
||||||
|
tableAutoReloadTask = toggleAutoReload(tableAutoReloadButton, interval, tableAutoReloadInput, _ => {
|
||||||
|
if (getSelection().length === 0) {
|
||||||
|
reload(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ready(_ => {
|
ready(_ => {
|
||||||
document.querySelectorAll("#repositories a").forEach(element => {
|
document.querySelectorAll("#repositories a").forEach(element => {
|
||||||
element.onclick = _ => {
|
element.onclick = _ => {
|
||||||
@ -289,5 +306,8 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
selectRepository();
|
selectRepository();
|
||||||
|
{% if autorefresh_intervals %}
|
||||||
|
toggleTableAutoReload();
|
||||||
|
{% endif %}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -137,6 +137,28 @@
|
|||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleAutoReload(toggle, interval, intervalSelector, callback) {
|
||||||
|
if (interval) {
|
||||||
|
toggle.checked = true; // toggle reload
|
||||||
|
} else {
|
||||||
|
interval = intervalSelector.querySelector(".active")?.dataset?.interval; // find active element
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interval) {
|
||||||
|
if (toggle.checked) {
|
||||||
|
// refresh UI
|
||||||
|
Array.from(intervalSelector.children).forEach(il => {
|
||||||
|
Array.from(il.children).forEach(el => el.classList.remove("active"));
|
||||||
|
});
|
||||||
|
intervalSelector.querySelector(`a[data-interval="${interval}"]`)?.classList?.add("active");
|
||||||
|
// finally create timer task
|
||||||
|
return setInterval(callback, interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // return null to assign to keep method sane
|
||||||
|
}
|
||||||
|
|
||||||
Date.prototype.toISOStringShort = function() {
|
Date.prototype.toISOStringShort = function() {
|
||||||
const pad = number => String(number).padStart(2, "0");
|
const pad = number => String(number).padStart(2, "0");
|
||||||
return `${this.getFullYear()}-${pad(this.getMonth() + 1)}-${pad(this.getDate())} ${pad(this.getHours())}:${pad(this.getMinutes())}:${pad(this.getSeconds())}`;
|
return `${this.getFullYear()}-${pad(this.getMonth() + 1)}-${pad(this.getDate())} ${pad(this.getHours())}:${pad(this.getMinutes())}:${pad(this.getSeconds())}`;
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
# pylint: disable=too-many-public-methods
|
||||||
import configparser
|
import configparser
|
||||||
import shlex
|
import shlex
|
||||||
import sys
|
import sys
|
||||||
@ -85,9 +86,10 @@ class Configuration(configparser.RawConfigParser):
|
|||||||
empty_lines_in_values=not allow_multi_key,
|
empty_lines_in_values=not allow_multi_key,
|
||||||
interpolation=ShellInterpolator(),
|
interpolation=ShellInterpolator(),
|
||||||
converters={
|
converters={
|
||||||
|
"intlist": lambda value: list(map(int, shlex.split(value))),
|
||||||
"list": shlex.split,
|
"list": shlex.split,
|
||||||
"path": self._convert_path,
|
"path": self._convert_path,
|
||||||
"pathlist": lambda value: [self._convert_path(element) for element in shlex.split(value)],
|
"pathlist": lambda value: list(map(self._convert_path, shlex.split(value))),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -236,6 +238,8 @@ class Configuration(configparser.RawConfigParser):
|
|||||||
|
|
||||||
# pylint and mypy are too stupid to find these methods
|
# pylint and mypy are too stupid to find these methods
|
||||||
# pylint: disable=missing-function-docstring,unused-argument
|
# pylint: disable=missing-function-docstring,unused-argument
|
||||||
|
def getintlist(self, *args: Any, **kwargs: Any) -> list[int]: ... # type: ignore[empty-body]
|
||||||
|
|
||||||
def getlist(self, *args: Any, **kwargs: Any) -> list[str]: ... # type: ignore[empty-body]
|
def getlist(self, *args: Any, **kwargs: Any) -> list[str]: ... # type: ignore[empty-body]
|
||||||
|
|
||||||
def getpath(self, *args: Any, **kwargs: Any) -> Path: ... # type: ignore[empty-body]
|
def getpath(self, *args: Any, **kwargs: Any) -> Path: ... # type: ignore[empty-body]
|
||||||
|
@ -324,6 +324,15 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
|||||||
"empty": False,
|
"empty": False,
|
||||||
"is_url": ["http", "https"],
|
"is_url": ["http", "https"],
|
||||||
},
|
},
|
||||||
|
"autorefresh_intervals": {
|
||||||
|
"type": "list",
|
||||||
|
"coerce": "list",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"coerce": "integer",
|
||||||
|
"min": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
"enable_archive_upload": {
|
"enable_archive_upload": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"coerce": "boolean",
|
"coerce": "boolean",
|
||||||
|
@ -51,6 +51,7 @@ __all__ = [
|
|||||||
"parse_version",
|
"parse_version",
|
||||||
"partition",
|
"partition",
|
||||||
"pretty_datetime",
|
"pretty_datetime",
|
||||||
|
"pretty_interval",
|
||||||
"pretty_size",
|
"pretty_size",
|
||||||
"safe_filename",
|
"safe_filename",
|
||||||
"srcinfo_property",
|
"srcinfo_property",
|
||||||
@ -353,6 +354,28 @@ def pretty_datetime(timestamp: datetime.datetime | float | int | None) -> str:
|
|||||||
return timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
return timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
|
||||||
|
def pretty_interval(interval: int) -> str:
|
||||||
|
"""
|
||||||
|
convert time interval to string
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interval(int): time interval in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: pretty printable interval as string
|
||||||
|
"""
|
||||||
|
minutes, seconds = divmod(interval, 60)
|
||||||
|
hours, minutes = divmod(minutes, 60)
|
||||||
|
return " ".join([
|
||||||
|
f"{value} {description}{"s" if value > 1 else ""}"
|
||||||
|
for value, description in [
|
||||||
|
(hours, "hour"),
|
||||||
|
(minutes, "minute"),
|
||||||
|
(seconds, "second"),
|
||||||
|
] if value > 0
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
def pretty_size(size: float | None, level: int = 0) -> str:
|
def pretty_size(size: float | None, level: int = 0) -> str:
|
||||||
"""
|
"""
|
||||||
convert size to string
|
convert size to string
|
||||||
|
@ -22,6 +22,7 @@ import aiohttp_jinja2
|
|||||||
from typing import Any, ClassVar
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
from ahriman.core.auth.helpers import authorized_userid
|
from ahriman.core.auth.helpers import authorized_userid
|
||||||
|
from ahriman.core.utils import pretty_interval
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.apispec import aiohttp_apispec
|
from ahriman.web.apispec import aiohttp_apispec
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
@ -37,6 +38,10 @@ class IndexView(BaseView):
|
|||||||
* control - HTML to insert for login control, HTML string, required
|
* control - HTML to insert for login control, HTML string, required
|
||||||
* enabled - whether authorization is enabled by configuration or not, boolean, required
|
* enabled - whether authorization is enabled by configuration or not, boolean, required
|
||||||
* username - authenticated username if any, string, null means not authenticated
|
* username - authenticated username if any, string, null means not authenticated
|
||||||
|
* autorefresh_intervals - auto refresh intervals, optional
|
||||||
|
* interval - auto refresh interval in milliseconds, integer, required
|
||||||
|
* is_active - is current interval active or not, boolean, required
|
||||||
|
* text - text representation of the interval (e.g. "30 seconds"), string, required
|
||||||
* docs_enabled - indicates if api docs is enabled, boolean, required
|
* docs_enabled - indicates if api docs is enabled, boolean, required
|
||||||
* index_url - url to the repository index, string, optional
|
* index_url - url to the repository index, string, optional
|
||||||
* repositories - list of repositories unique identifiers, required
|
* repositories - list of repositories unique identifiers, required
|
||||||
@ -66,8 +71,18 @@ class IndexView(BaseView):
|
|||||||
"username": auth_username,
|
"username": auth_username,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
autorefresh_intervals = [
|
||||||
|
{
|
||||||
|
"interval": interval * 1000, # milliseconds
|
||||||
|
"is_active": index == 0,
|
||||||
|
"text": pretty_interval(interval),
|
||||||
|
}
|
||||||
|
for index, interval in enumerate(self.configuration.getintlist("web", "autorefresh_intervals", fallback=[]))
|
||||||
|
]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"auth": auth,
|
"auth": auth,
|
||||||
|
"autorefresh_intervals": sorted(autorefresh_intervals, key=lambda interval: interval["interval"]),
|
||||||
"docs_enabled": aiohttp_apispec is not None,
|
"docs_enabled": aiohttp_apispec is not None,
|
||||||
"index_url": self.configuration.get("web", "index_url", fallback=None),
|
"index_url": self.configuration.get("web", "index_url", fallback=None),
|
||||||
"repositories": [
|
"repositories": [
|
||||||
|
@ -133,6 +133,14 @@ def test_dump_architecture_specific(configuration: Configuration) -> None:
|
|||||||
assert dump["build"]["archbuild_flags"] == "hello flag"
|
assert dump["build"]["archbuild_flags"] == "hello flag"
|
||||||
|
|
||||||
|
|
||||||
|
def test_getintlist(configuration: Configuration) -> None:
|
||||||
|
"""
|
||||||
|
must extract list of integers
|
||||||
|
"""
|
||||||
|
configuration.set_option("build", "test_int_list", "1 42 3")
|
||||||
|
assert configuration.getintlist("build", "test_int_list") == [1, 42, 3]
|
||||||
|
|
||||||
|
|
||||||
def test_getlist(configuration: Configuration) -> None:
|
def test_getlist(configuration: Configuration) -> None:
|
||||||
"""
|
"""
|
||||||
must return list of string correctly
|
must return list of string correctly
|
||||||
|
@ -10,8 +10,8 @@ from unittest.mock import call as MockCall
|
|||||||
|
|
||||||
from ahriman.core.exceptions import BuildError, CalledProcessError, OptionError, UnsafeRunError
|
from ahriman.core.exceptions import BuildError, CalledProcessError, OptionError, UnsafeRunError
|
||||||
from ahriman.core.utils import check_output, check_user, dataclass_view, enum_values, extract_user, filter_json, \
|
from ahriman.core.utils import check_output, check_user, dataclass_view, enum_values, extract_user, filter_json, \
|
||||||
full_version, minmax, package_like, parse_version, partition, pretty_datetime, pretty_size, safe_filename, \
|
full_version, minmax, package_like, parse_version, partition, pretty_datetime, pretty_interval, pretty_size, \
|
||||||
srcinfo_property, srcinfo_property_list, trim_package, utcnow, walk
|
safe_filename, srcinfo_property, srcinfo_property_list, trim_package, utcnow, walk
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.package_source import PackageSource
|
from ahriman.models.package_source import PackageSource
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
@ -341,6 +341,18 @@ def test_pretty_datetime_empty() -> None:
|
|||||||
assert pretty_datetime(None) == ""
|
assert pretty_datetime(None) == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_pretty_interval() -> None:
|
||||||
|
"""
|
||||||
|
must generate string from interval
|
||||||
|
"""
|
||||||
|
assert pretty_interval(1) == "1 second"
|
||||||
|
assert pretty_interval(42) == "42 seconds"
|
||||||
|
assert pretty_interval(62) == "1 minute 2 seconds"
|
||||||
|
assert pretty_interval(121) == "2 minutes 1 second"
|
||||||
|
assert pretty_interval(3600) == "1 hour"
|
||||||
|
assert pretty_interval(7242) == "2 hours 42 seconds"
|
||||||
|
|
||||||
|
|
||||||
def test_pretty_size_bytes() -> None:
|
def test_pretty_size_bytes() -> None:
|
||||||
"""
|
"""
|
||||||
must generate bytes string for bytes value
|
must generate bytes string for bytes value
|
||||||
|
Reference in New Issue
Block a user