mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-23 23:07:17 +00:00
migration of jinja tempaltes to bootstrap (#30)
This commit is contained in:
parent
f49894107a
commit
19ba2ba8e5
@ -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]
|
||||
|
@ -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>
|
||||
|
||||
{% 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>
|
||||
<td>{{ package.version }}</td>
|
||||
<td>{{ package.timestamp }}</td>
|
||||
<td class="status package-{{ package.status }}">{{ package.status }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr class="package">
|
||||
<td colspan="100%">In order to see statuses you must login first</td>
|
||||
<tbody>
|
||||
{% if authorized %}
|
||||
{% for package in packages %}
|
||||
<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="table-{{ package.status_color }}">{{ package.status }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="100%">In order to see statuses you must login first</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
|
44
package/share/ahriman/email-index.jinja2
Normal file
44
package/share/ahriman/email-index.jinja2
Normal 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>
|
@ -1,9 +0,0 @@
|
||||
<script>
|
||||
const modal = document.getElementById('login-form');
|
||||
|
||||
window.onclick = function(event) {
|
||||
if (event.target === modal) {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
}
|
||||
</script>
|
@ -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>
|
@ -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 %}
|
||||
<h1>Archlinux user repository</h1>
|
||||
|
||||
<section class="element">
|
||||
{% 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>
|
||||
{% 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 class="container">
|
||||
<h1>Archlinux user repository</h1>
|
||||
</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>
|
||||
|
||||
</html>
|
||||
|
@ -1,3 +0,0 @@
|
||||
<section class="element">
|
||||
<input type="search" id="search" onkeyup="searchInTable()" placeholder="search for package" title="search for package"/>
|
||||
</section>
|
@ -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>
|
@ -1 +0,0 @@
|
||||
<script src="https://www.kryogenix.org/code/browser/sorttable/sorttable.js"></script>
|
@ -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>
|
||||
|
4
setup.py
4
setup.py
@ -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",
|
||||
]),
|
||||
],
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
"""
|
||||
value = Path(self.get(section, key))
|
||||
if self.path is None or value.is_absolute():
|
||||
try:
|
||||
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 self.path.parent / value
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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:
|
||||
"""
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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()))
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user