migration of jinja tempaltes to bootstrap (#30)

This commit is contained in:
Evgenii Alekseev 2021-09-05 05:27:58 +03:00 committed by GitHub
parent ecf45bc3bb
commit 9b8c9b2b2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 313 additions and 412 deletions

View File

@ -29,8 +29,9 @@ target =
target =
[email]
full_template_path = /usr/share/ahriman/repo-index.jinja2
no_empty_report = yes
template_path = /usr/share/ahriman/repo-index.jinja2
template_path = /usr/share/ahriman/email-index.jinja2
ssl = disabled
[html]

View File

@ -3,14 +3,18 @@
<head>
<title>{{ repository }}</title>
{% include "style.jinja2" %}
<meta name="viewport" content="width=device-width, initial-scale=1">
{% include "sorttable.jinja2" %}
{% include "search.jinja2" %}
<script src="https://kit.fontawesome.com/0d6d6d5226.js" crossorigin="anonymous"></script>
<link href="https://unpkg.com/bootstrap-table@1.18.3/dist/bootstrap-table.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
{% include "style.jinja2" %}
</head>
<body>
<div class="root">
<div class="container">
<h1>ahriman
{% if authorized %}
<img src="https://img.shields.io/badge/version-{{ version }}-informational" alt="{{ version }}">
@ -18,58 +22,100 @@
<img src="https://img.shields.io/badge/service%20status-{{ service.status }}-{{ service.status_color }}" alt="{{ service.status }}" title="{{ service.timestamp }}">
{% endif %}
</h1>
</div>
{% include "login-form.jinja2" %}
{% include "login-form-hide.jinja2" %}
{% include "search-line.jinja2" %}
<section class="element">
<table class="sortable search-table">
<tr class="header">
<th>package base</th>
<th>packages</th>
<th>version</th>
<th>last update</th>
<th>status</th>
<div class="container">
<table id="packages" class="table table-striped table-hover" cellspacing="0"
data-toggle="table"
data-pagination="true"
data-page-siz="10"
data-page-list="[10, 25, 50, 100, all]"
data-search="true"
data-show-columns="true"
data-show-export="true"
data-sortable="true">
<thead class="table-primary">
<tr>
<th data-sortable="true">package base</th>
<th data-sortable="true">packages</th>
<th data-sortable="true">version</th>
<th data-sortable="true">last update</th>
<th data-sortable="true">status</th>
</tr>
</thead>
<tbody>
{% if authorized %}
{% for package in packages %}
<tr class="package">
<td class="include-search"><a href="{{ package.web_url }}" title="{{ package.base }}">{{ package.base }}</a></td>
<td class="include-search">{{ package.packages|join("<br>"|safe) }}</td>
<tr>
<td><a href="{{ package.web_url }}" title="{{ package.base }}">{{ package.base }}</a></td>
<td>{{ package.packages|join("<br>"|safe) }}</td>
<td>{{ package.version }}</td>
<td>{{ package.timestamp }}</td>
<td class="status package-{{ package.status }}">{{ package.status }}</td>
<td class="table-{{ package.status_color }}">{{ package.status }}</td>
</tr>
{% endfor %}
{% else %}
<tr class="package">
<tr>
<td colspan="100%">In order to see statuses you must login first</td>
</tr>
{% endif %}
</tbody>
</table>
</section>
</div>
<footer>
<ul class="navigation">
<li><a href="https://github.com/arcan1s/ahriman" title="sources">ahriman</a></li>
<li><a href="https://github.com/arcan1s/ahriman/releases" title="releases list">releases</a></li>
<li><a href="https://github.com/arcan1s/ahriman/issues" title="issues tracker">report a bug</a></li>
{% if auth_enabled %}
<li class="right">
{% if auth_username is not none %}
<form action="/logout" method="post">
<button class="login" type="submit">logout ({{ auth_username }})</button>
</form>
{% else %}
<button class="login" onclick="document.getElementById('login-form').style.display='block'">login</button>
{% endif %}
</li>
{% endif %}
<div class="container">
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
<ul class="nav">
<li><a class="nav-link" href="https://github.com/arcan1s/ahriman" title="sources">ahriman</a></li>
<li><a class="nav-link" href="https://github.com/arcan1s/ahriman/releases" title="releases list">releases</a></li>
<li><a class="nav-link" href="https://github.com/arcan1s/ahriman/issues" title="issues tracker">report a bug</a></li>
</ul>
{% if auth_enabled %}
{% if auth_username is none %}
<button type="button" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#loginForm" style="text-decoration: none">login</button>
{% else %}
<form action="/logout" method="post">
<button type="submit" class="btn btn-link" style="text-decoration: none">logout ({{ auth_username }})</button>
</form>
{% endif %}
{% endif %}
</footer>
</div>
<div id="loginForm" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog modal-login">
<div class="modal-content">
<form action="/login" method="post">
<div class="modal-header">
<h4 class="modal-title">login</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="username" class="col-form-label">username</label>
<input id="username" type="text" class="form-control" placeholder="enter username" name="username" required>
</div>
<div class="form-group">
<label for="password" class="col-form-label">password</label>
<input id="password" type="password" class="form-control" placeholder="enter username" name="password" required>
</div>
</div>
<div class="modal-footer">
<input type="submit" class="btn btn-primary" value="Login">
</div>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
<script src="https://unpkg.com/tableexport.jquery.plugin/tableExport.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj" crossorigin="anonymous"></script>
<script src="https://unpkg.com/bootstrap-table@1.18.3/dist/bootstrap-table.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.18.3/dist/extensions/export/bootstrap-table-export.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,44 @@
{#simplified version of full report#}
<!doctype html>
<html lang="en">
<head>
<title>{{ repository }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
{% include "style.jinja2" %}
</head>
<body>
<div class="container">
<table id="packages" class="table table-striped" cellspacing="0">
<thead class="table-primary">
<tr>
<th>package</th>
<th>version</th>
<th>archive size</th>
<th>installed size</th>
<th>build date</th>
</tr>
</thead>
<tbody>
{% for package in packages %}
<tr>
<td><a href="{{ link_path }}/{{ package.filename }}" title="{{ package.name }}">{{ package.name }}</a></td>
<td>{{ package.version }}</td>
<td>{{ package.archive_size }}</td>
<td>{{ package.installed_size }}</td>
<td>{{ package.build_date }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</body>
</html>

View File

@ -1,9 +0,0 @@
<script>
const modal = document.getElementById('login-form');
window.onclick = function(event) {
if (event.target === modal) {
modal.style.display = "none";
}
}
</script>

View File

@ -1,18 +0,0 @@
{#idea is from here https://www.w3schools.com/howto/howto_css_login_form.asp#}
<div id="login-form" class="modal-login-form">
<form class="modal-login-form-content animate" action="/login" method="post">
<div class="login-container">
<label for="username"><b>username</b></label>
<input type="text" placeholder="enter username" name="username" required>
<label for="password"><b>password</b></label>
<input type="password" placeholder="enter password" name="password" required>
<button class="login" type="submit">login</button>
</div>
<div class="login-container">
<button class="cancel" onclick="document.getElementById('login-form').style.display='none'">cancel</button>
</div>
</form>
</div>

View File

@ -3,66 +3,82 @@
<head>
<title>{{ repository }}</title>
{% include "style.jinja2" %}
<meta name="viewport" content="width=device-width, initial-scale=1">
{% if extended_report %}
{% include "sorttable.jinja2" %}
{% include "search.jinja2" %}
{% endif %}
<script src="https://kit.fontawesome.com/0d6d6d5226.js" crossorigin="anonymous"></script>
<link href="https://unpkg.com/bootstrap-table@1.18.3/dist/bootstrap-table.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
{% include "style.jinja2" %}
</head>
<body>
<div class="root">
{% if extended_report %}
<div class="container">
<h1>Archlinux user repository</h1>
</div>
<section class="element">
<div class="container">
{% if pgp_key is not none %}
<p>This repository is signed with <a href="http://keys.gnupg.net/pks/lookup?search=0x{{ pgp_key }}&fingerprint=on&op=index" title="key search">{{ pgp_key }}</a> by default.</p>
<p>This repository is signed with <a href="https://pgp.mit.edu/pks/lookup?search=0x{{ pgp_key }}&fingerprint=on&op=index" title="key search">{{ pgp_key }}</a> by default.</p>
{% endif %}
<code>
$ cat /etc/pacman.conf<br>
[{{ repository }}]<br>
Server = {{ link_path }}<br>
SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Package{% if has_package_signed %}Required{% else %}Never{% endif %} TrustedOnly
</code>
</section>
<pre>$ cat /etc/pacman.conf
[{{ repository }}]
Server = {{ link_path }}
SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Package{% if has_package_signed %}Required{% else %}Never{% endif %} TrustedOnly</pre>
</div>
{% include "search-line.jinja2" %}
{% endif %}
<section class="element">
<table class="sortable search-table">
<tr class="header">
<th>package</th>
<th>version</th>
<th>archive size</th>
<th>installed size</th>
<th>build date</th>
<div class="container">
<table id="packages" class="table table-striped table-hover" cellspacing="0"
data-toggle="table"
data-pagination="true"
data-page-siz="10"
data-page-list="[10, 25, 50, 100, all]"
data-search="true"
data-show-columns="true"
data-show-export="true"
data-sortable="true">
<thead class="table-primary">
<tr>
<th data-sortable="true">package</th>
<th data-sortable="true">version</th>
<th data-sortable="true">archive size</th>
<th data-sortable="true">installed size</th>
<th data-sortable="true">build date</th>
</tr>
</thead>
<tbody>
{% for package in packages %}
<tr class="package">
<td class="include-search"><a href="{{ link_path }}/{{ package.filename }}" title="{{ package.name }}">{{ package.name }}</a></td>
<tr>
<td><a href="{{ link_path }}/{{ package.filename }}" title="{{ package.name }}">{{ package.name }}</a></td>
<td>{{ package.version }}</td>
<td>{{ package.archive_size }}</td>
<td>{{ package.installed_size }}</td>
<td>{{ package.build_date }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
</div>
{% if extended_report %}
<footer>
<ul class="navigation">
<div class="container">
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
<ul class="nav">
{% if homepage is not none %}
<li><a href="{{ homepage }}" title="homepage">Homepage</a></li>
<li><a class="nav-link" href="{{ homepage }}" title="homepage">Homepage</a></li>
{% endif %}
</ul>
</footer>
{% endif %}
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
<script src="https://unpkg.com/tableexport.jquery.plugin/tableExport.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj" crossorigin="anonymous"></script>
<script src="https://unpkg.com/bootstrap-table@1.18.3/dist/bootstrap-table.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.18.3/dist/extensions/export/bootstrap-table-export.min.js"></script>
</body>
</html>

View File

@ -1,3 +0,0 @@
<section class="element">
<input type="search" id="search" onkeyup="searchInTable()" placeholder="search for package" title="search for package"/>
</section>

View File

@ -1,26 +0,0 @@
<script type="text/javascript">
function searchInTable() {
const input = document.getElementById("search");
const filter = input.value.toLowerCase();
const tables = document.getElementsByClassName("search-table");
for (let i = 0; i < tables.length; i++) {
const trs = tables[i].getElementsByTagName("tr");
// from 1 coz of header
for (let i = 1; i < trs.length; i++) {
let tr = trs[i].getElementsByClassName("include-search");
let display = "none";
for (let j = 0; j < tr.length; j++) {
if (tr[j].tagName.toLowerCase() === "td") {
let contains = (element) => tr[j].innerHTML.toLowerCase().indexOf(element) > -1
if (filter.some(contains)) {
display = "";
break;
}
}
}
trs[i].style.display = display;
}
}
}
</script>

View File

@ -1 +0,0 @@
<script src="https://www.kryogenix.org/code/browser/sorttable/sorttable.js"></script>

View File

@ -1,215 +1 @@
<style>
:root {
--color-building: 255, 255, 146;
--color-failed: 255, 94, 94;
--color-pending: 255, 255, 146;
--color-success: 94, 255, 94;
--color-unknown: 225, 225, 225;
--color-header: 200, 200, 255;
--color-hover: 255, 255, 225;
--color-line-blue: 235, 235, 255;
--color-line-white: 255, 255, 255;
}
@keyframes blink-building {
0% { background-color: rgba(var(--color-building), 1.0); }
10% { background-color: rgba(var(--color-building), 0.9); }
20% { background-color: rgba(var(--color-building), 0.8); }
30% { background-color: rgba(var(--color-building), 0.7); }
40% { background-color: rgba(var(--color-building), 0.6); }
50% { background-color: rgba(var(--color-building), 0.5); }
60% { background-color: rgba(var(--color-building), 0.4); }
70% { background-color: rgba(var(--color-building), 0.3); }
80% { background-color: rgba(var(--color-building), 0.2); }
90% { background-color: rgba(var(--color-building), 0.1); }
100% { background-color: rgba(var(--color-building), 0.0); }
}
div.root {
width: 70%;
padding: 15px 15% 0;
}
section.element, footer {
width: 100%;
padding: 10px 0;
}
code, input, table {
width: inherit;
}
/* table description */
th, td {
padding: 5px;
}
tr.package:nth-child(odd) {
background-color: rgba(var(--color-line-white), 1.0);
}
tr.package:nth-child(even) {
background-color: rgba(var(--color-line-blue), 1.0);
}
tr.package:hover {
background-color: rgba(var(--color-hover), 1.0);
}
tr.header{
background-color: rgba(var(--color-header), 1.0);
}
td.status {
text-align: center;
}
td.package-unknown {
background-color: rgba(var(--color-unknown), 1.0);
}
td.package-pending {
background-color: rgba(var(--color-pending), 1.0);
}
td.package-building {
background-color: rgba(var(--color-building), 1.0);
animation-name: blink-building;
animation-duration: 1s;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-direction: alternate;
}
td.package-failed {
background-color: rgba(var(--color-failed), 1.0);
}
td.package-success {
background-color: rgba(var(--color-success), 1.0);
}
li.service-unknown {
background-color: rgba(var(--color-unknown), 1.0);
}
li.service-building {
background-color: rgba(var(--color-building), 1.0);
animation-name: blink-building;
animation-duration: 1s;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-direction: alternate;
}
li.service-failed {
background-color: rgba(var(--color-failed), 1.0);
}
li.service-success {
background-color: rgba(var(--color-success), 1.0);
}
/* navigation footer description */
ul.navigation {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
background-color: rgba(var(--color-header), 1.0);
}
ul.navigation li {
float: left;
}
ul.navigation li.right {
float: right;
}
ul.navigation li a {
display: block;
color: black;
text-align: center;
text-decoration: none;
padding: 14px 16px;
}
ul.navigation li a:hover {
opacity: 0.6;
}
/* login button in footer and modal page */
button.login {
background-color: rgba(var(--color-header), 1.0);
padding: 14px 16px;
border: none;
cursor: pointer;
width: 100%;
}
button.login:hover {
opacity: 0.6;
}
button.cancel {
background-color: rgba(var(--color-failed), 1.0);
padding: 14px 16px;
border: none;
cursor: pointer;
width: 100%;
}
button.cancel:hover {
opacity: 0.6;
}
/* modal page inputs and containers */
input[type=text], input[type=password] {
width: 100%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
box-sizing: border-box;
}
.login-container {
padding: 14px 16px;
}
span.password {
float: right;
padding-top: 16px;
}
.modal-login-form {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgb(0, 0, 0);
background-color: rgba(0, 0, 0, 0.4);
padding-top: 60px;
}
.modal-login-form-content {
background-color: #fefefe;
margin: 5% auto 15% auto;
border: 1px solid #888;
width: 25%;
}
/* modal page animation */
.animate {
-webkit-animation: animatezoom 0.6s;
animation: animatezoom 0.6s
}
@-webkit-keyframes animatezoom {
from {-webkit-transform: scale(0)}
to {-webkit-transform: scale(1)}
}
@keyframes animatezoom {
from {transform: scale(0)}
to {transform: scale(1)}
}
</style>
<style></style>

View File

@ -66,10 +66,8 @@ setup(
]),
("share/ahriman", [
"package/share/ahriman/build-status.jinja2",
"package/share/ahriman/email-index.jinja2",
"package/share/ahriman/repo-index.jinja2",
"package/share/ahriman/search.jinja2",
"package/share/ahriman/search-line.jinja2",
"package/share/ahriman/sorttable.jinja2",
"package/share/ahriman/style.jinja2",
]),
],

View File

@ -188,7 +188,7 @@ def _set_key_import_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser = root.add_parser("key-import", help="import PGP key",
description="import PGP key from public sources to repository user",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--key-server", help="key server for key import", default="keys.gnupg.net")
parser.add_argument("--key-server", help="key server for key import", default="pgp.mit.edu")
parser.add_argument("key", help="PGP key to import from public server")
parser.set_defaults(handler=handlers.KeyImport, architecture=[""], lock=None, no_report=True)
return parser

View File

@ -24,7 +24,7 @@ import logging
from logging.config import fileConfig
from pathlib import Path
from typing import Dict, List, Optional, Type
from typing import Any, Dict, List, Optional, Type
class Configuration(configparser.RawConfigParser):
@ -41,6 +41,8 @@ class Configuration(configparser.RawConfigParser):
ARCHITECTURE_SPECIFIC_SECTIONS = ["build", "html", "rsync", "s3", "sign", "web"]
_UNSET = object()
def __init__(self) -> None:
"""
default constructor. In the most cases must not be called directly
@ -109,15 +111,21 @@ class Configuration(configparser.RawConfigParser):
return []
return raw.split()
def getpath(self, section: str, key: str) -> Path:
def getpath(self, section: str, key: str, fallback: Any = _UNSET) -> Path:
"""
helper to generate absolute configuration path for relative settings value
:param section: section name
:param key: key name
:param fallback: optional fallback value
:return: absolute path according to current path configuration
"""
try:
value = Path(self.get(section, key))
if self.path is None or value.is_absolute():
except (configparser.NoOptionError, configparser.NoSectionError):
if fallback is self._UNSET:
raise
value = fallback
if self.path is None or not isinstance(value, Path) or value.is_absolute():
return value
return self.path.parent / value

View File

@ -54,6 +54,9 @@ class Email(Report, JinjaTemplate):
Report.__init__(self, architecture, configuration)
JinjaTemplate.__init__(self, "email", configuration)
self.full_template_path = configuration.getpath("email", "full_template_path", fallback=None)
self.template_path = configuration.getpath("email", "template_path")
# base smtp settings
self.host = configuration.get("email", "host")
self.no_empty_report = configuration.getboolean("email", "no_empty_report", fallback=True)
@ -100,6 +103,9 @@ class Email(Report, JinjaTemplate):
"""
if self.no_empty_report and not built_packages:
return
text = self.make_html(built_packages, False)
attachments = {"index.html": self.make_html(packages, True)}
text = self.make_html(built_packages, self.template_path)
if self.full_template_path is not None:
attachments = {"index.html": self.make_html(packages, self.full_template_path)}
else:
attachments = {}
self._send(text, attachments)

View File

@ -41,6 +41,7 @@ class HTML(Report, JinjaTemplate):
JinjaTemplate.__init__(self, "html", configuration)
self.report_path = configuration.getpath("html", "path")
self.template_path = configuration.getpath("html", "template_path")
def generate(self, packages: Iterable[Package], built_packages: Iterable[Package]) -> None:
"""
@ -48,5 +49,5 @@ class HTML(Report, JinjaTemplate):
:param packages: list of packages to generate report
:param built_packages: list of packages which has just been built
"""
html = self.make_html(packages, True)
html = self.make_html(packages, self.template_path)
self.report_path.write_text(html)

View File

@ -19,6 +19,7 @@
#
import jinja2
from pathlib import Path
from typing import Callable, Dict, Iterable
from ahriman.core.configuration import Configuration
@ -59,7 +60,6 @@ class JinjaTemplate:
:ivar name: repository name
:ivar default_pgp_key: default PGP key
:ivar sign_targets: targets to sign enabled in configuration
:ivar template_path: path to directory with jinja templates
"""
def __init__(self, section: str, configuration: Configuration) -> None:
@ -69,7 +69,6 @@ class JinjaTemplate:
:param configuration: configuration instance
"""
self.link_path = configuration.get(section, "link_path")
self.template_path = configuration.getpath(section, "template_path")
# base template vars
self.homepage = configuration.get(section, "homepage", fallback=None)
@ -77,16 +76,16 @@ class JinjaTemplate:
self.sign_targets, self.default_pgp_key = GPG.sign_options(configuration)
def make_html(self, packages: Iterable[Package], extended_report: bool) -> str:
def make_html(self, packages: Iterable[Package], template_path: Path) -> str:
"""
generate report for the specified packages
:param packages: list of packages to generate report
:param extended_report: include additional blocks to the report
:param template_path: path to jinja template
"""
# idea comes from https://stackoverflow.com/a/38642558
loader = jinja2.FileSystemLoader(searchpath=self.template_path.parent)
loader = jinja2.FileSystemLoader(searchpath=template_path.parent)
environment = jinja2.Environment(loader=loader, autoescape=True)
template = environment.get_template(self.template_path.name)
template = environment.get_template(template_path.name)
content = [
{
@ -107,7 +106,6 @@ class JinjaTemplate:
comparator: Callable[[Dict[str, str]], str] = lambda item: item["filename"]
return template.render(
extended_report=extended_report,
homepage=self.homepage,
link_path=self.link_path,
has_package_signed=SignSettings.Packages in self.sign_targets,

View File

@ -58,6 +58,21 @@ class BuildStatusEnum(Enum):
return "success"
return "inactive"
def bootstrap_color(self) -> str:
"""
converts itself to bootstrap color
:return: bootstrap color
"""
if self == BuildStatusEnum.Pending:
return "warning"
if self == BuildStatusEnum.Building:
return "warning"
if self == BuildStatusEnum.Failed:
return "danger"
if self == BuildStatusEnum.Success:
return "success"
return "secondary"
class BuildStatus:
"""

View File

@ -44,6 +44,7 @@ class IndexView(BaseView):
* licenses, sorted list of strings
* packages, sorted list of strings
* status, string based on enum value
* status_color, string based on enum value
* timestamp, pretty printed datetime, string
* version, string
* web_url, string
@ -70,6 +71,7 @@ class IndexView(BaseView):
"licenses": package.licenses,
"packages": list(sorted(package.packages)),
"status": status.status.value,
"status_color": status.status.bootstrap_color(),
"timestamp": pretty_datetime(status.timestamp),
"version": package.version,
"web_url": package.web_url

View File

@ -13,7 +13,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
:return: generated arguments for these test cases
"""
args.key = "0xE989490C"
args.key_server = "keys.gnupg.net"
args.key_server = "pgp.mit.edu"
return args

View File

@ -105,6 +105,21 @@ def test_generate_with_built(configuration: Configuration, package_ahriman: Pack
send_mock.assert_called_once()
def test_generate_with_built_and_full_path(
configuration: Configuration,
package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must generate report with built packages
"""
send_mock = mocker.patch("ahriman.core.report.email.Email._send")
report = Email("x86_64", configuration)
report.full_template_path = report.template_path
report.generate([package_ahriman], [package_ahriman])
send_mock.assert_called_once()
def test_generate_no_empty(configuration: Configuration, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must not generate report with built packages if no_empty_report is set

View File

@ -7,13 +7,6 @@ def test_generate(configuration: Configuration, package_ahriman: Package) -> Non
"""
must generate html report
"""
path = configuration.getpath("html", "template_path")
report = JinjaTemplate("html", configuration)
assert report.make_html([package_ahriman], extended_report=False)
def test_generate_extended(configuration: Configuration, package_ahriman: Package) -> None:
"""
must generate extended html report
"""
report = JinjaTemplate("html", configuration)
assert report.make_html([package_ahriman], extended_report=True)
assert report.make_html([package_ahriman], path)

View File

@ -69,7 +69,7 @@ def test_download_key(gpg: GPG, mocker: MockerFixture) -> None:
must download the key from public server
"""
requests_mock = mocker.patch("requests.get")
gpg.download_key("keys.gnupg.net", "0xE989490C")
gpg.download_key("pgp.mit.edu", "0xE989490C")
requests_mock.assert_called_once()
@ -79,7 +79,7 @@ def test_download_key_failure(gpg: GPG, mocker: MockerFixture) -> None:
"""
mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError())
with pytest.raises(requests.exceptions.HTTPError):
gpg.download_key("keys.gnupg.net", "0xE989490C")
gpg.download_key("pgp.mit.edu", "0xE989490C")
def test_import_key(gpg: GPG, mocker: MockerFixture) -> None:
@ -89,7 +89,7 @@ def test_import_key(gpg: GPG, mocker: MockerFixture) -> None:
mocker.patch("ahriman.core.sign.gpg.GPG.download_key", return_value="key")
check_output_mock = mocker.patch("ahriman.core.sign.gpg.GPG._check_output")
gpg.import_key("keys.gnupg.net", "0xE989490C")
gpg.import_key("pgp.mit.edu", "0xE989490C")
check_output_mock.assert_has_calls([
mock.call("gpg", "--import", input_data="key", exception=None, logger=pytest.helpers.anyvar(int)),
mock.call("gpg", "--quick-lsign-key", "0xE989490C", exception=None, logger=pytest.helpers.anyvar(int))

View File

@ -1,5 +1,7 @@
import configparser
from pathlib import Path
import pytest
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
@ -49,6 +51,25 @@ def test_absolute_path_for_relative(configuration: Configuration) -> None:
assert result.name == path.name
def test_path_with_fallback(configuration: Configuration) -> None:
"""
must return fallback path
"""
path = Path("a")
assert configuration.getpath("some", "option", fallback=path).name == str(path)
assert configuration.getpath("some", "option", fallback=None) is None
def test_path_without_fallback(configuration: Configuration) -> None:
"""
must raise exception without fallback
"""
with pytest.raises(configparser.NoSectionError):
assert configuration.getpath("some", "option")
with pytest.raises(configparser.NoOptionError):
assert configuration.getpath("build", "option")
def test_dump(configuration: Configuration) -> None:
"""
dump must not be empty

View File

@ -59,14 +59,10 @@ def test_get_local_files(s3: S3, resource_path_root: Path) -> None:
Path("models/package_ahriman_srcinfo"),
Path("models/package_tpacpi-bat-git_srcinfo"),
Path("models/package_yay_srcinfo"),
Path("web/templates/search-line.jinja2"),
Path("web/templates/build-status.jinja2"),
Path("web/templates/login-form.jinja2"),
Path("web/templates/login-form-hide.jinja2"),
Path("web/templates/email-index.jinja2"),
Path("web/templates/repo-index.jinja2"),
Path("web/templates/sorttable.jinja2"),
Path("web/templates/style.jinja2"),
Path("web/templates/search.jinja2"),
])
local_files = list(sorted(s3.get_local_files(resource_path_root).keys()))

View File

@ -16,6 +16,18 @@ def test_build_status_enum_badges_color() -> None:
assert status.badges_color() in SUPPORTED_COLORS
def test_build_status_enum_bootstrap_color() -> None:
"""
status color must be one of shields.io supported
"""
SUPPORTED_COLORS = [
"primary", "secondary", "success", "danger", "warning", "info", "light", "dark"
]
for status in BuildStatusEnum:
assert status.bootstrap_color() in SUPPORTED_COLORS
def test_build_status_init_1() -> None:
"""
must construct status object from None