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 = target =
[email] [email]
full_template_path = /usr/share/ahriman/repo-index.jinja2
no_empty_report = yes no_empty_report = yes
template_path = /usr/share/ahriman/repo-index.jinja2 template_path = /usr/share/ahriman/email-index.jinja2
ssl = disabled ssl = disabled
[html] [html]

View File

@ -3,14 +3,18 @@
<head> <head>
<title>{{ repository }}</title> <title>{{ repository }}</title>
{% include "style.jinja2" %} <meta name="viewport" content="width=device-width, initial-scale=1">
{% include "sorttable.jinja2" %} <script src="https://kit.fontawesome.com/0d6d6d5226.js" crossorigin="anonymous"></script>
{% include "search.jinja2" %} <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> </head>
<body> <body>
<div class="root">
<div class="container">
<h1>ahriman <h1>ahriman
{% if authorized %} {% if authorized %}
<img src="https://img.shields.io/badge/version-{{ version }}-informational" alt="{{ version }}"> <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 }}"> <img src="https://img.shields.io/badge/service%20status-{{ service.status }}-{{ service.status_color }}" alt="{{ service.status }}" title="{{ service.timestamp }}">
{% endif %} {% endif %}
</h1> </h1>
</div>
{% include "login-form.jinja2" %} <div class="container">
{% include "login-form-hide.jinja2" %} <table id="packages" class="table table-striped table-hover" cellspacing="0"
{% include "search-line.jinja2" %} data-toggle="table"
data-pagination="true"
<section class="element"> data-page-siz="10"
<table class="sortable search-table"> data-page-list="[10, 25, 50, 100, all]"
<tr class="header"> data-search="true"
<th>package base</th> data-show-columns="true"
<th>packages</th> data-show-export="true"
<th>version</th> data-sortable="true">
<th>last update</th> <thead class="table-primary">
<th>status</th> <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> </tr>
</thead>
{% if authorized %} <tbody>
{% for package in packages %} {% if authorized %}
<tr class="package"> {% for package in packages %}
<td class="include-search"><a href="{{ package.web_url }}" title="{{ package.base }}">{{ package.base }}</a></td> <tr>
<td class="include-search">{{ package.packages|join("<br>"|safe) }}</td> <td><a href="{{ package.web_url }}" title="{{ package.base }}">{{ package.base }}</a></td>
<td>{{ package.version }}</td> <td>{{ package.packages|join("<br>"|safe) }}</td>
<td>{{ package.timestamp }}</td> <td>{{ package.version }}</td>
<td class="status package-{{ package.status }}">{{ package.status }}</td> <td>{{ package.timestamp }}</td>
</tr> <td class="table-{{ package.status_color }}">{{ package.status }}</td>
{% endfor %}
{% else %}
<tr class="package">
<td colspan="100%">In order to see statuses you must login first</td>
</tr> </tr>
{% endif %} {% endfor %}
</table> {% else %}
</section> <tr>
<td colspan="100%">In order to see statuses you must login first</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<footer> <div class="container">
<ul class="navigation"> <footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
<li><a href="https://github.com/arcan1s/ahriman" title="sources">ahriman</a></li> <ul class="nav">
<li><a href="https://github.com/arcan1s/ahriman/releases" title="releases list">releases</a></li> <li><a class="nav-link" href="https://github.com/arcan1s/ahriman" title="sources">ahriman</a></li>
<li><a href="https://github.com/arcan1s/ahriman/issues" title="issues tracker">report a bug</a></li> <li><a class="nav-link" href="https://github.com/arcan1s/ahriman/releases" title="releases list">releases</a></li>
{% if auth_enabled %} <li><a class="nav-link" href="https://github.com/arcan1s/ahriman/issues" title="issues tracker">report a bug</a></li>
<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 %}
</ul> </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> </footer>
</div> </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> </body>
</html> </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> <head>
<title>{{ repository }}</title> <title>{{ repository }}</title>
{% include "style.jinja2" %} <meta name="viewport" content="width=device-width, initial-scale=1">
{% if extended_report %} <script src="https://kit.fontawesome.com/0d6d6d5226.js" crossorigin="anonymous"></script>
{% include "sorttable.jinja2" %} <link href="https://unpkg.com/bootstrap-table@1.18.3/dist/bootstrap-table.min.css" rel="stylesheet">
{% include "search.jinja2" %} <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">
{% endif %}
{% include "style.jinja2" %}
</head> </head>
<body> <body>
<div class="root">
{% if extended_report %}
<h1>Archlinux user repository</h1>
<section class="element"> <div class="container">
{% if pgp_key is not none %} <h1>Archlinux user repository</h1>
<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>
{% 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>
{% 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>
</tr>
{% for package in packages %}
<tr class="package">
<td class="include-search"><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 %}
</table>
</section>
{% if extended_report %}
<footer>
<ul class="navigation">
{% if homepage is not none %}
<li><a href="{{ homepage }}" title="homepage">Homepage</a></li>
{% endif %}
</ul>
</footer>
{% endif %}
</div> </div>
<div class="container">
{% if pgp_key is not none %}
<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 %}
<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>
<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>
<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>
<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 class="nav-link" href="{{ homepage }}" title="homepage">Homepage</a></li>
{% endif %}
</ul>
</footer>
</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> </body>
</html> </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> <style></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>

View File

@ -66,10 +66,8 @@ setup(
]), ]),
("share/ahriman", [ ("share/ahriman", [
"package/share/ahriman/build-status.jinja2", "package/share/ahriman/build-status.jinja2",
"package/share/ahriman/email-index.jinja2",
"package/share/ahriman/repo-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", "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", parser = root.add_parser("key-import", help="import PGP key",
description="import PGP key from public sources to repository user", description="import PGP key from public sources to repository user",
formatter_class=argparse.ArgumentDefaultsHelpFormatter) 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.add_argument("key", help="PGP key to import from public server")
parser.set_defaults(handler=handlers.KeyImport, architecture=[""], lock=None, no_report=True) parser.set_defaults(handler=handlers.KeyImport, architecture=[""], lock=None, no_report=True)
return parser return parser

View File

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

View File

@ -54,6 +54,9 @@ class Email(Report, JinjaTemplate):
Report.__init__(self, architecture, configuration) Report.__init__(self, architecture, configuration)
JinjaTemplate.__init__(self, "email", 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 # base smtp settings
self.host = configuration.get("email", "host") self.host = configuration.get("email", "host")
self.no_empty_report = configuration.getboolean("email", "no_empty_report", fallback=True) 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: if self.no_empty_report and not built_packages:
return return
text = self.make_html(built_packages, False) text = self.make_html(built_packages, self.template_path)
attachments = {"index.html": self.make_html(packages, True)} 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) self._send(text, attachments)

View File

@ -41,6 +41,7 @@ class HTML(Report, JinjaTemplate):
JinjaTemplate.__init__(self, "html", configuration) JinjaTemplate.__init__(self, "html", configuration)
self.report_path = configuration.getpath("html", "path") 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: 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 packages: list of packages to generate report
:param built_packages: list of packages which has just been built :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) self.report_path.write_text(html)

View File

@ -19,6 +19,7 @@
# #
import jinja2 import jinja2
from pathlib import Path
from typing import Callable, Dict, Iterable from typing import Callable, Dict, Iterable
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
@ -59,7 +60,6 @@ class JinjaTemplate:
:ivar name: repository name :ivar name: repository name
:ivar default_pgp_key: default PGP key :ivar default_pgp_key: default PGP key
:ivar sign_targets: targets to sign enabled in configuration :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: def __init__(self, section: str, configuration: Configuration) -> None:
@ -69,7 +69,6 @@ class JinjaTemplate:
:param configuration: configuration instance :param configuration: configuration instance
""" """
self.link_path = configuration.get(section, "link_path") self.link_path = configuration.get(section, "link_path")
self.template_path = configuration.getpath(section, "template_path")
# base template vars # base template vars
self.homepage = configuration.get(section, "homepage", fallback=None) 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) 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 generate report for the specified packages
:param packages: list of packages to generate report :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 # 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) environment = jinja2.Environment(loader=loader, autoescape=True)
template = environment.get_template(self.template_path.name) template = environment.get_template(template_path.name)
content = [ content = [
{ {
@ -107,7 +106,6 @@ class JinjaTemplate:
comparator: Callable[[Dict[str, str]], str] = lambda item: item["filename"] comparator: Callable[[Dict[str, str]], str] = lambda item: item["filename"]
return template.render( return template.render(
extended_report=extended_report,
homepage=self.homepage, homepage=self.homepage,
link_path=self.link_path, link_path=self.link_path,
has_package_signed=SignSettings.Packages in self.sign_targets, has_package_signed=SignSettings.Packages in self.sign_targets,

View File

@ -58,6 +58,21 @@ class BuildStatusEnum(Enum):
return "success" return "success"
return "inactive" 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: class BuildStatus:
""" """

View File

@ -44,6 +44,7 @@ class IndexView(BaseView):
* licenses, sorted list of strings * licenses, sorted list of strings
* packages, sorted list of strings * packages, sorted list of strings
* status, string based on enum value * status, string based on enum value
* status_color, string based on enum value
* timestamp, pretty printed datetime, string * timestamp, pretty printed datetime, string
* version, string * version, string
* web_url, string * web_url, string
@ -70,6 +71,7 @@ class IndexView(BaseView):
"licenses": package.licenses, "licenses": package.licenses,
"packages": list(sorted(package.packages)), "packages": list(sorted(package.packages)),
"status": status.status.value, "status": status.status.value,
"status_color": status.status.bootstrap_color(),
"timestamp": pretty_datetime(status.timestamp), "timestamp": pretty_datetime(status.timestamp),
"version": package.version, "version": package.version,
"web_url": package.web_url "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 :return: generated arguments for these test cases
""" """
args.key = "0xE989490C" args.key = "0xE989490C"
args.key_server = "keys.gnupg.net" args.key_server = "pgp.mit.edu"
return args return args

View File

@ -105,6 +105,21 @@ def test_generate_with_built(configuration: Configuration, package_ahriman: Pack
send_mock.assert_called_once() 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: 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 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 must generate html report
""" """
path = configuration.getpath("html", "template_path")
report = JinjaTemplate("html", configuration) report = JinjaTemplate("html", configuration)
assert report.make_html([package_ahriman], extended_report=False) assert report.make_html([package_ahriman], path)
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)

View File

@ -69,7 +69,7 @@ def test_download_key(gpg: GPG, mocker: MockerFixture) -> None:
must download the key from public server must download the key from public server
""" """
requests_mock = mocker.patch("requests.get") 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() 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()) mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError())
with pytest.raises(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: 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") mocker.patch("ahriman.core.sign.gpg.GPG.download_key", return_value="key")
check_output_mock = mocker.patch("ahriman.core.sign.gpg.GPG._check_output") 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([ check_output_mock.assert_has_calls([
mock.call("gpg", "--import", input_data="key", exception=None, logger=pytest.helpers.anyvar(int)), 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)) 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 from pathlib import Path
import pytest
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
@ -49,6 +51,25 @@ def test_absolute_path_for_relative(configuration: Configuration) -> None:
assert result.name == path.name 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: def test_dump(configuration: Configuration) -> None:
""" """
dump must not be empty 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_ahriman_srcinfo"),
Path("models/package_tpacpi-bat-git_srcinfo"), Path("models/package_tpacpi-bat-git_srcinfo"),
Path("models/package_yay_srcinfo"), Path("models/package_yay_srcinfo"),
Path("web/templates/search-line.jinja2"),
Path("web/templates/build-status.jinja2"), Path("web/templates/build-status.jinja2"),
Path("web/templates/login-form.jinja2"), Path("web/templates/email-index.jinja2"),
Path("web/templates/login-form-hide.jinja2"),
Path("web/templates/repo-index.jinja2"), Path("web/templates/repo-index.jinja2"),
Path("web/templates/sorttable.jinja2"),
Path("web/templates/style.jinja2"), Path("web/templates/style.jinja2"),
Path("web/templates/search.jinja2"),
]) ])
local_files = list(sorted(s3.get_local_files(resource_path_root).keys())) 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 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: def test_build_status_init_1() -> None:
""" """
must construct status object from None must construct status object from None