Implemented tools as applets, Added Docker CCTV page, Fixed small issues

Update app.py, uuid-generator.yml, and 47 more files...
This commit is contained in:
2025-02-20 17:24:05 +01:00
parent bd96d85699
commit 0e91b5ed96
43 changed files with 889 additions and 337 deletions

94
app.py
View File

@@ -9,22 +9,43 @@ from flask import render_template
from minify_html import minify
from werkzeug.exceptions import HTTPException
from website.content import reload_content_items, get_articles, get_projects, get_tools, sanitize_input_tags
from website.content import get_articles, get_projects, get_tools, sanitize_input_tags, load_content_items, get_content, \
get_applets
from website.contributors import reload_contributors_data, get_contributors_data
from website.domains import ALLOWED_DOMAINS
from website.l10n.utils import get_user_lang, localize, reload_strings, l10n_url_abs, l10n_url_switch, L10N, \
DEFAULT_LANG
from website.renderers.applet import render_applet_scripts, render_applet_head
from website.renderers.button import render_button
from website.renderers.headings import render_heading, render_h2, render_h1, render_h3
from website.renderers.code import render_code_block
from website.renderers.headings import render_heading, render_h2, render_h1, render_h3, render_h4
from website.renderers.paragraph import render_paragraph
from website.renderers.lists import render_list_ul
from website.renderers.splide import render_splide
from website.sidebar import reload_sidebar_entries, get_sidebar_entries
from website.sitemap import reload_sitemap_entries, get_sitemap_entries
# try:
# from rich import print
# except ImportError:
# pass
try:
from rich import print
except ImportError:
pass
if os.environ.get('NP_HTML_POST_PROCESS', "NONE") == "MINIFY":
print("Using 'minify' as HTML post-processor")
def post_process_html(html: str) -> str:
return minify(html).replace("> <", "><")
elif os.environ.get('NP_HTML_POST_PROCESS', "NONE") == "BS4":
print("Using 'BeautifulSoup4' as HTML post-processor")
def post_process_html(html: str) -> str:
return BeautifulSoup(html, features="html.parser").prettify()
else:
print("Using no HTML post-processor")
def post_process_html(html: str) -> str:
return html
app = Flask(
@@ -72,24 +93,35 @@ def inject_processors():
domain_host=request.headers['Host'],
domain_tld=request.headers['Host'].split('.')[-1] if request.headers['Host'] in ALLOWED_DOMAINS else "lu",
domain_url_root=request.url_root,
# L10N
l10n=localize,
l10n_url_abs=l10n_url_abs,
l10n_url_switch=l10n_url_switch,
# Sidebar
get_sidebar_entries=get_sidebar_entries,
# Content
get_content=get_content,
get_articles=get_articles,
get_projects=get_projects,
get_tools=get_tools,
# Renderers
render_button=render_button,
render_heading=render_heading,
render_h1=render_h1,
render_h2=render_h2,
render_h3=render_h3,
render_h4=render_h4,
render_paragraph=render_paragraph,
render_list_ul=render_list_ul,
render_splide=render_splide,
render_applet_scripts=render_applet_scripts,
render_applet_head=render_applet_head,
render_code_block=render_code_block,
# Commons
url_for=url_for,
escape=escape,
@@ -118,7 +150,7 @@ def route_sitemap():
@app.route('/fr/', defaults={'lang': "fr"})
def route_root(lang: Optional[str]):
user_lang = get_user_lang(lang, request.headers.get("HTTP_ACCEPT_LANGUAGE"))
return minify(render_template(
return post_process_html(render_template(
"pages/root.jinja",
user_lang=user_lang,
raw_lang=lang,
@@ -132,7 +164,7 @@ def route_root(lang: Optional[str]):
@app.route('/fr/contact/', defaults={'lang': "fr"})
def route_contact(lang: Optional[str]):
user_lang = get_user_lang(lang, request.headers.get("HTTP_ACCEPT_LANGUAGE"))
return minify(render_template(
return post_process_html(render_template(
"pages/contact.jinja",
user_lang=user_lang,
raw_lang=lang,
@@ -152,7 +184,7 @@ def route_content(lang: Optional[str]):
except ValueError:
requested_tags = None
return minify(render_template(
return post_process_html(render_template(
"pages/project_index.jinja",
user_lang=user_lang,
raw_lang=lang,
@@ -178,7 +210,7 @@ def route_content_project(lang: Optional[str], project_id: str):
error_code = 404
if error_key is not None:
return minify(render_template(
return post_process_html(render_template(
"pages/error.jinja",
user_lang=user_lang,
raw_lang=lang,
@@ -188,7 +220,7 @@ def route_content_project(lang: Optional[str], project_id: str):
error_code=error_code,
)).replace("> <", "><"), error_code
else:
return minify(render_template(
return post_process_html(render_template(
"projects/" + project_id + ".jinja",
user_lang=user_lang,
raw_lang=lang,
@@ -210,7 +242,7 @@ def route_tools_index(lang: Optional[str]):
except ValueError:
requested_tags = None
return minify(render_template(
return post_process_html(render_template(
"pages/tools_index.jinja",
user_lang=user_lang,
raw_lang=lang,
@@ -236,7 +268,7 @@ def route_tools_page(lang: Optional[str], tool_id: str):
error_code = 404
if error_key is not None:
return minify(render_template(
return post_process_html(render_template(
"pages/error.jinja",
user_lang=user_lang,
raw_lang=lang,
@@ -246,14 +278,15 @@ def route_tools_page(lang: Optional[str], tool_id: str):
error_code=error_code,
)).replace("> <", "><"), error_code
else:
return minify(render_template(
"tools/" + tool_id + ".jinja",
return post_process_html(render_template(
"pages/tools_page.jinja",
user_lang=user_lang,
raw_lang=lang,
request_path=request.path,
standalone="standalone" in request.args,
tool_data=get_tools().get(tool_id),
tool_id=tool_id,
applet_data=get_applets().get(get_tools().get(tool_id).applet_id),
)).replace("> <", "><")
@@ -262,7 +295,7 @@ def route_tools_page(lang: Optional[str], tool_id: str):
@app.route('/fr/about/', defaults={'lang': "fr"})
def route_about(lang: Optional[str]):
user_lang = get_user_lang(lang, request.headers.get("HTTP_ACCEPT_LANGUAGE"))
return minify(render_template(
return post_process_html(render_template(
"pages/about.jinja",
user_lang=user_lang,
raw_lang=lang,
@@ -276,7 +309,7 @@ def route_about(lang: Optional[str]):
@app.route('/fr/privacy/', defaults={'lang': "fr"})
def route_privacy(lang: Optional[str]):
user_lang = get_user_lang(lang, request.headers.get("HTTP_ACCEPT_LANGUAGE"))
return minify(render_template(
return post_process_html(render_template(
"pages/privacy.jinja",
user_lang=user_lang,
raw_lang=lang,
@@ -290,7 +323,7 @@ def route_privacy(lang: Optional[str]):
@app.route('/fr/links/', defaults={'lang': "fr"})
def route_links(lang: Optional[str]):
user_lang = get_user_lang(lang, request.headers.get("HTTP_ACCEPT_LANGUAGE"))
return minify(render_template(
return post_process_html(render_template(
"pages/links.jinja",
user_lang=user_lang,
raw_lang=lang,
@@ -304,7 +337,7 @@ def route_links(lang: Optional[str]):
@app.route('/fr/debug/', defaults={'lang': "fr"})
def route_debug(lang: Optional[str]):
user_lang = get_user_lang(lang, request.headers.get("HTTP_ACCEPT_LANGUAGE"))
return minify(render_template(
return post_process_html(render_template(
"pages/debug.jinja",
user_lang=user_lang,
raw_lang=lang,
@@ -323,7 +356,7 @@ def handle_exception(e: Exception):
if isinstance(e, HTTPException):
error_code = e.code
return minify(render_template(
return post_process_html(render_template(
"pages/error.jinja",
user_lang=DEFAULT_LANG,
raw_lang=DEFAULT_LANG,
@@ -335,7 +368,7 @@ def handle_exception(e: Exception):
if __name__ == '__main__':
reload_content_items()
load_content_items()
reload_strings(os.path.join(os.getcwd(), "data/strings/"))
reload_sidebar_entries(os.path.join(os.getcwd(), "data/sidebar.yml"))
reload_contributors_data(os.path.join(os.getcwd(), "data/contributors.yml"))
@@ -362,22 +395,3 @@ if __name__ == '__main__':
#debug=False,
load_dotenv=False
)
# return BeautifulSoup(render_template(
# "pages/root.jinja",
# lang=user_lang,
# raw_lang=lang,
# request_path=request.path,
# standalone="standalone" in request.args,
# ), features="html.parser").prettify()
# try:
# from minify_html import minify
# FORCE_NON_DEBUG = False
# except ImportError:
# from bs4 import BeautifulSoup
# FORCE_NON_DEBUG = True
#
# def minify(html):
# return BeautifulSoup(html, features="html.parser").prettify()
# debug=False if FORCE_NON_DEBUG else True,

View File

@@ -0,0 +1,8 @@
applets:
- id: "uuid-generator"
resources:
scripts:
- "uuid-generator.mjs"
stylesheets:
- "uuid-generator.css"

View File

@@ -0,0 +1,35 @@
projects:
- id: "circuitpython-custom-fs"
metadata:
head:
title_key: "meta.title"
description_key: "meta.description"
opengraph:
title_key: "meta.title"
description_key: "meta.description"
type: null
url: null
image_url: "/resources/NibblePoker/images/content/circuitpython-ebyte-e32/main.png"
image_type: null
twitter:
title_key: "meta.title"
description_key: "meta.description"
index:
priority: 105
enable: true
title_key: "meta.title"
preamble_key: "meta.description"
image_url: "/resources/NibblePoker/images/content/circuitpython-ebyte-e32/main.png"
image_alt_key: ""
general:
icon: "fab fa-python"
title_key: "meta.title"
subtitle_key: "article.subtitle"
tags:
- "experiments"
- "electronic"
- "python"
- "circuitpython"
languages:
- "python"

View File

@@ -1,31 +0,0 @@
head:
title_key: "meta.title"
description_key: "meta.description"
opengraph:
title_key: "meta.title"
description_key: "meta.description"
type: null
url: null
image_url: "/resources/NibblePoker/images/content/circuitpython-ebyte-e32/main.png"
image_type: null
twitter:
title_key: "meta.title"
description_key: "meta.description"
index:
priority: 105
enable: true
title_key: "meta.title"
preamble_key: "meta.description"
image_url: "/resources/NibblePoker/images/content/circuitpython-ebyte-e32/main.png"
image_alt_key: ""
general:
icon: "fab fa-python"
title_key: "meta.title"
subtitle_key: "article.subtitle"
tags:
- "experiments"
- "electronic"
- "python"
- "circuitpython"
languages:
- "python"

View File

@@ -0,0 +1,34 @@
projects:
- id: "docker-mini-cctv-nvr"
metadata:
head:
title_key: "meta.title"
description_key: "meta.description"
opengraph:
title_key: "meta.title"
description_key: "meta.description"
type: null
url: null
image_url: "/resources/NibblePoker/images/content/docker-mini-cctv-nvr/main.png"
image_type: null
twitter:
title_key: "meta.title"
description_key: "meta.description"
index:
priority: 105
enable: true
title_key: "meta.title"
preamble_key: "meta.description"
image_url: "/resources/NibblePoker/images/content/docker-mini-cctv-nvr/main.png"
image_alt_key: ""
general:
icon: "fab fa-docker"
title_key: "meta.title"
subtitle_key: "article.subtitle"
tags:
- "docker"
languages:
- "docker"
- "php"
- "python"

View File

@@ -42,13 +42,13 @@
abs_href: "/tools"
icon: fad fa-toolbox
active_id: tools
has_new_until_utc: 0
has_new_until_utc: 1760986472
- title_key: text.downloads
raw_href: "https://files.nibblepoker.lu/"
icon: fad fa-download
active_id: ""
has_new_until_utc: 1760986472
has_new_until_utc: 0
-

View File

@@ -0,0 +1,72 @@
# EN - Docker Mini CCTV NVR
meta.title: "Mini Dockerized CCTV NVR"
meta.description: "Mini docker stack that allows you to easily record, clean and serve CCTV recordings made
over RSTP while using a minimal amount of system resources."
intro.title: "Introduction"
intro.1: "A mini docker stack that allows you to easily record, clean and serve CCTV recordings made
over RSTP while using a minimal amount of system resources."
intro.2: "This stack is mainly intended to be used as a backup when other and more complete solutions crash or
need to be shutdown.This simple docker stack aims to provide you with a simple,
lightweight and robust NVR for all of your RTSP-based CCTV cameras."
preamble.title: "Preamble"
preamble.1: "This stack records the camera's streams as-is and doesn't re-encode or compress it which uses more disk space.
See \"Usage statistics example\" for an example."
preamble.2: "If served out of your LAN, the web server should be behind a secure reverse-proxy that requires authentication."
setup.title: "Setup"
setup.1: "All of the setup is done through environment variables in the docker-compose.yml file."
setup.2: "It should only take 2-3 minutes if you already have the RTSP URL on hand.<br>
If you don't have them, you should see your camera's user manual and test the URLs with VLC."
setup.camera.title: "Cameras"
setup.camera.1: "Each recording container needs to be given a RSTP stream URL and a unique folder
into which the recordings will go."
setup.camera.2: "The URL must be given via the <span class=\"code\">NP_CCTV_URL</span> environment variable,
and the output folder via a mounted volume that is mounted as <span class=\"code\">/data</span> in the container."
setup.camera.3: "This example will use the <span class=\"code\">rtsp://user:password@address:554/sub-path</span>
URL and will put its recordings in <span class=\"code\">./recordings/cam1.</span>"
setup.cleaner.title: "Cleaner"
setup.cleaner.1: "The cleaner script named cleaner.py only requires you to set 1 environment variable named
<span class=\"code\">NP_MAX_FILE_AGE_HOURS</span> to the max amount of hours any recording should be kept."
setup.cleaner.2: "If not set, the script will simply clean any recordings older than 72 hours."
setup.web.title: "Web interface"
setup.web.1: "The web interface provides more customization options, but at its core,
it only requires the camera's environment variables to be set."
setup.web.2: "Each camera requires one of the following environment variable:<br>
&nbsp;&nbsp;<span class=\"code\">NP_CAM_&lt;camId&gt; = &lt;Camera's name&gt;</span>"
setup.web.3: "Here is an example for <span class=\"code\">cam1</span> if named as <span class=\"code\">Camera #1</span>:<br>
&nbsp;&nbsp;<span class=\"code\">NP_CAM_cam1 = Camera #1</span>"
setup.web.vars.title: "Other variables"
setup.web.vars.description.title: "Page's title"
setup.web.vars.description.footer: "Page's footer HTML content"
startup.title: "Startup"
startup.1: "Once you have finished setting up the stack, you can simply run the following command:"
#docker-compose up --build -d
screenshots.title: "Screenshots"
statistics.title: "Usage statistics example"
statistics.1: "NanoPi R4S 4GB"
statistics.1.1: "Uses 0.008 kWh / 8 Watts with other containers and USB HDD & USB SSD"
statistics.2: "4 IP Cameras"
statistics.2.1: "All H.256 4k RTSP TCP streams"
statistics.2.2: "Around 220 GB of data per day"
statistics.2.2.1: "Around 20.4 Mbit/s or 2.6 MB/s"
statistics.3: "Less than 200MB of RAM usage"
statistics.3.1: "~32 MB per recorder"
statistics.3.2: "4 MB for cleaner"
statistics.3.3: "4 MB for web server"
statistics.4: "Uses ~10% of CPU on average over 6 cores"
statistics.4.1: "Average of 15% per recorder"
statistics.4.2: "Average of 1-5% on cleaner and web server"
license.title: "License"
license.1: "This software, as well as the Simplette CSS Stylesheet used for the web interface are both licensed under Unlicense."
commons.example: "Example"

View File

@@ -1,4 +1,17 @@
# EN - UUID Generator
option.count: "UUID/GUID count"
meta.title: "UUID Generator"
type.label: "UUID Type"
type.uuid4: "UUID4 / GUID4"
option.count: "UUID Count"
option.hyphen: "Add hyphens"
option.guid_brackets: "Add GUID brackets"
generate: "Generate"
download.raw: "Raw"
download.json: "JSON"
download.yaml: "YAML"

View File

@@ -0,0 +1,5 @@
# FR - Docker Mini CCTV NVR
meta.title: "Mini Dockerized CCTV NVR"
meta.description: "Mini docker stack that allows you to easily record, clean and serve CCTV recordings made
over RSTP while using a minimal amount of system resources."

View File

@@ -1,4 +1,17 @@
# FR - UUID Generator
meta.title: "Générateur d'UUID"
type.label: "Type d'UUID"
type.uuid4: "UUID4 / GUID4"
option.count: "Nombre d'UUID/GUID"
option.hyphen: "Ajouter trait d'union"
option.guid_brackets: "Ajouter accolades pour GUID"
generate: "Générer"
download.raw: "Brut"
download.json: "JSON"
download.yaml: "YAML"

View File

@@ -25,6 +25,6 @@ metadata:
subtitle_key: "article.subtitle"
tags:
- "undefined"
data:
resources:
scripts:
- "epr_main.js"

View File

@@ -25,7 +25,7 @@ metadata:
subtitle_key: "article.subtitle"
tags:
- "undefined"
data:
resources:
scripts:
- "svg-to-png.mjs"
stylesheets:

View File

@@ -1,4 +1,8 @@
metadata:
tools:
- id: "uuid-generator"
applet_id: "uuid-generator"
metadata:
head:
title_key: "meta.title"
description_key: "meta.description"
@@ -25,8 +29,3 @@ metadata:
subtitle_key: "article.subtitle"
tags:
- "undefined"
data:
scripts:
- "uuid-generator.mjs"
stylesheets:
- "uuid-generator.css"

View File

@@ -10,4 +10,4 @@ locked-dict
Werkzeug~=3.0.4
#gunicorn
waitress~=3.0.0
waitress~=3.0.2

View File

@@ -39,8 +39,8 @@ popd
:js-uuidgenerator-minify
echo Minifying UUID Generator
pushd %CD%
cd %~dp0\..\static\resources\NibblePoker\tools\uuid-generator\
echo ^> static\resources\NibblePoker\tools\svg-to-png\svg-to-png.mjs
cd %~dp0\..\static\resources\NibblePoker\applets\uuid-generator\
echo ^> static\resources\NibblePoker\applets\uuid-generator\uuid-generator.mjs
call "%~dp0node_modules\.bin\rollup" uuid-generator.mjs --file uuid-generator.js
call "%~dp0node_modules\.bin\terser" uuid-generator.js -c -m -o uuid-generator.min.js
popd

View File

@@ -0,0 +1,134 @@
/**
* Generates a random UUID4 and returns its string representation
* @returns {`${string}-${string}-${string}-${string}-${string}`}
*/
export function generateUUID4(addHyphens, addGuidBrackets) {
let uuid4 = crypto.randomUUID();
if(!addHyphens) {
uuid4 = uuid4.replace(/-/g, "");
}
if(addGuidBrackets) {
uuid4 = "{" + uuid4 + "}";
}
return uuid4;
}
// Tool-centric stuff
{
/** @type {HTMLSelectElement} */
const eOptionTypeSelect = document.querySelector("select#uuid-generator-option-type");
/** @type {HTMLInputElement} */
const eOptionCountInput = document.querySelector("input#uuid-generator-option-count");
/** @type {HTMLInputElement} */
const eOptionHyphenInput = document.querySelector("input#uuid-generator-option-hyphens");
/** @type {HTMLInputElement} */
const eOptionGuidBracketsInput = document.querySelector("input#uuid-generator-option-guid-brackets");
/** @type {HTMLElement} */
const eGenerateButton = document.querySelector("#uuid-generator-generate");
/** @type {HTMLElement} */
const eDownloadRawButton = document.querySelector("#uuid-generator-download-raw");
/** @type {HTMLElement} */
const eDownloadJsonButton = document.querySelector("#uuid-generator-download-json");
/** @type {HTMLElement} */
const eDownloadYamlButton = document.querySelector("#uuid-generator-download-yaml");
/** @type {HTMLTextAreaElement} */
const ePreviewTextArea = document.querySelector("textarea#uuid-generator-preview");
let lastUUIDs = [];
/** @returns {number} */
function getDesiredCount() {
let desiredCount = null;
try {
desiredCount = parseInt(eOptionCountInput.value);
} catch (e) {
console.error(e);
}
if(desiredCount === null) {
desiredCount = 1;
}
if(desiredCount < 1) {
desiredCount = 1;
}
if(desiredCount > 1000) {
desiredCount = 1000;
}
return desiredCount;
}
function changeDesiredCount(difference = 0) {
if(difference !== 0) {
eOptionCountInput.value = getDesiredCount() + difference;
}
eOptionCountInput.value = getDesiredCount();
}
function downloadStringAsFile(content, filename, contentType) {
const blob = new Blob([content], { type: contentType });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
window.onload = function () {
eGenerateButton.addEventListener("click", function() {
ePreviewTextArea.value = "";
let desiredCount = getDesiredCount();
let uuidGenerator = generateUUID4;
let addHyphens = eOptionHyphenInput.checked;
let addGuidBrackets = eOptionGuidBracketsInput.checked;
lastUUIDs = [];
for(let i= 0; i < desiredCount; i++) {
lastUUIDs.push(uuidGenerator(addHyphens, addGuidBrackets));
ePreviewTextArea.value += uuidGenerator(addHyphens, addGuidBrackets) + "\n";
}
ePreviewTextArea.value = lastUUIDs.join("\n");
});
eOptionCountInput.addEventListener("change", function() {
changeDesiredCount(0);
});
eOptionCountInput.addEventListener("mousewheel", function(e) {
// Handling wheel scroll on count field.
if(e.wheelDelta < 0) {
changeDesiredCount(-1);
} else {
changeDesiredCount(1);
}
});
eDownloadRawButton.addEventListener("click", function() {
if (lastUUIDs.length <= 0) {
return;
}
downloadStringAsFile(lastUUIDs.join("\n"), "uuids.txt", "text/plain");
});
eDownloadJsonButton.addEventListener("click", function() {
if (lastUUIDs.length <= 0) {
return;
}
downloadStringAsFile(JSON.stringify(lastUUIDs, null, 4), "uuids.json", "application/json");
});
eDownloadYamlButton.addEventListener("click", function() {
if (lastUUIDs.length <= 0) {
return;
}
downloadStringAsFile("- \"" + lastUUIDs.join("\"\n- \"") + "\"", "uuids.yaml", "text/yaml");
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,38 @@
# CircuitPython - Custom File Systems
The goal of this experiment was to try and give people a strong, clear, and documented starting point for
future experiments that may require virtual file systems and block-level devices on CircuitPython devices.
For example, by using the blank examples, you can easily create a bootstrapping code and file system that connects
securely to a remote server and pulls code directly from it without ever having to touch the MCU's flash storage. \
The second main advantage is that this project can serve as a robust educational tool. \
Due to the permissive nature of Python and CircuitPython's APIs, it lets people easily test out different designs and
mechanisms for their file systems without running the risk of corrupting unrelated data or bricking their device. \
Additionally, it is possible to manipulate and add logging to many of the methods which allows you to see and understand
the inner workings of CircuitPython, filesystems and BLD devices themselves.
Le but de cette expérience était de fournir un point de départ solide, clair et documenté pour
de futures expériences nécessitant des systèmes de fichiers virtuels, ou des périphériques de bloc sur
des appareils utilisant CircuitPython.
Par exemple, en utilisant les exemples modèles, il est possible de créer facilement un code de démarrage et
un système de fichiers qui vont connecter de manière sécurisée à un serveur distant, et y récupérer directement du
code sans jamais avoir à toucher à la mémoire flash du MCU.
Le deuxième avantage majeur est que ce projet peut servir d'outil éducatif.
En effet, les APIs extrêmement permissives de Python et CircuitPython permettent à leurs utilisateurs de tester
facilement différents designs et mécanismes pour leurs systèmes de fichiers sans risquer de corrompre des
données ou de rendre leur appareil inutilisable. \
De plus, vous pouvez très facilement ajouter des messages de débogage et manipuler plusieurs méthodes, ce
qui permet de voir et de comprendre le fonctionnement interne de CircuitPython, des systèmes de fichiers et
des périphériques de bloc en eux-mêmes.
## ???
## Media coverage
https://blog.adafruit.com/2023/02/22/icymi-python-on-microcontrollers-newsletter-new-raspberry-pi-debug-probe-circuitpython-8-0-2-and-much-more-circuitpython-python-micropython-icymi-raspberry_pi/

View File

@@ -1,61 +0,0 @@
/**
* Generates a random UUID4 and returns its string representation
* @returns {`${string}-${string}-${string}-${string}-${string}`}
*/
export function generateUUID4() {
return crypto.randomUUID();
}
// Tool-centric stuff
{
/** @type {HTMLSelectElement} */
const eOptionTypeSelect = document.querySelector("select#uuid-generator-option-type");
/** @type {HTMLInputElement} */
const eOptionCountInput = document.querySelector("input#uuid-generator-option-count");
/** @type {HTMLInputElement} */
const eOptionHyphenInput = document.querySelector("input#uuid-generator-option-hyphens");
/** @type {HTMLElement} */
const eGenerateButton = document.querySelector("#uuid-generator-generate");
/** @type {HTMLElement} */
const eDownloadButton = document.querySelector("#uuid-generator-download");
/** @type {HTMLTextAreaElement} */
const ePreviewTextArea = document.querySelector("textarea#uuid-generator-preview");
/** @returns {number} */
function getDesiredCount() {
let desiredCount = null;
try {
desiredCount = parseInt(eOptionCountInput.value);
} catch (e) {
console.error(e);
}
if(desiredCount === null) {
desiredCount = 1;
}
if(desiredCount < 1) {
desiredCount = 1;
}
if(desiredCount > 1000) {
desiredCount = 1000;
}
return desiredCount;
}
window.onload = function () {
eGenerateButton.addEventListener("click", function() {
ePreviewTextArea.value = "";
let desiredCount = getDesiredCount();
let uuidGenerator = generateUUID4;
for(let i= 0; i < desiredCount; i++) {
ePreviewTextArea.value += uuidGenerator() + "\n";
}
});
eDownloadButton.addEventListener("click", function() {
//eFileDropInput.click();
});
}
}

View File

@@ -0,0 +1,44 @@
<label for="uuid-generator-option-type" class="mr-xs">{{ l10n("type.label", "uuid-generator", user_lang) }}:</label>
<select name="uuid-generator-option-type" id="uuid-generator-option-type" class="p-xxs border r-s">
<option value="type-uuid4" selected>{{ l10n("type.uuid4", "uuid-generator", user_lang) }}</option>
<!--<option value="type-guid">{{ l10n("type.guid", "uuid-generator", user_lang) }}</option>-->
</select>
<br>
<label for="uuid-generator-option-count" class="mr-xs">{{ l10n("option.count", "uuid-generator", user_lang) }}:</label>
<input id="uuid-generator-option-count" class="p-xxs border r-s" type="number" value="4" min="1" max="1000">
<br>
<label for="uuid-generator-option-hyphens" class="mr-xxs">{{ l10n("option.hyphen", "uuid-generator", user_lang) }}:</label>
<input id="uuid-generator-option-hyphens" class="r-m border" type="checkbox" checked>
<br>
<label for="uuid-generator-option-guid-brackets" class="mr-xxs">{{ l10n("option.guid_brackets", "uuid-generator", user_lang) }}:</label>
<input id="uuid-generator-option-guid-brackets" class="r-m border" type="checkbox">
<hr class="subtle">
<button id="uuid-generator-generate" class="p-xs r-s border b-light success">
<i class="fa-duotone fa-solid fa-gears mr-xs"></i>{{ l10n("generate", "uuid-generator", user_lang) }}
</button>
<button class="p-xs r-s border b-light primary rr-0 br-0">
<i class="fa-duotone fa-solid fa-download"></i>
</button>
<button id="uuid-generator-download-raw" class="p-xs r-s border b-light primary ml-0 r-0 br-0">
{{ l10n("download.raw", "uuid-generator", user_lang) }}
</button>
<button id="uuid-generator-download-json" class="p-xs r-s border b-light primary ml-0 r-0 br-0">
{{ l10n("download.json", "uuid-generator", user_lang) }}
</button>
<button id="uuid-generator-download-yaml" class="p-xs r-s border b-light primary ml-0 rl-0">
{{ l10n("download.yaml", "uuid-generator", user_lang) }}
</button>
<hr class="subtle">
<label for="uuid-generator-preview" class="d-none">{{ l10n("preview.label", "uuid-generator", user_lang) }}:</label>
<textarea name="uuid-generator-preview" id="uuid-generator-preview" rows="16" class="w-full border r-s"></textarea>

View File

@@ -10,7 +10,8 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="alternate icon" href="/favicon.ico">
<link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/FontAwesomePro/6.5.1/css/all.min.css">
<link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/NibblePoker/StandardCSS/nibblepoker.min.css">
<!--<link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/NibblePoker/StandardCSS/nibblepoker.min.css">-->
<link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/NibblePoker/IndevCSS/nibblepoker.min.css">
<link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/Quantum/Quantum.min.css">
<meta charset="UTF-8">

View File

@@ -19,7 +19,8 @@
{% block extra_preloads %}{% endblock %}
<link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/FontAwesomePro/6.5.1/css/all.min.css">
<link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/NibblePoker/StandardCSS/nibblepoker.min.css">
<!--<link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/NibblePoker/StandardCSS/nibblepoker.min.css">-->
<link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/NibblePoker/IndevCSS/nibblepoker.min.css">
<link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/Quantum/Quantum.min.css">
{% block extra_stylesheets %}{% endblock %}

View File

@@ -0,0 +1,5 @@
<code class="code ox-auto w-full d-inline-block position-relative language-{{ code_language }}">
{% for code_line in code_lines %}
<span class="code-line t-nowrap">{{ code_line }}</span><br>
{% endfor %}
</code>

View File

@@ -0,0 +1,9 @@
<ul class="l-bullets l-bullet-manual">
{% for list_item in list_items %}
{% if list_item.__class__.__name__ == 'list' %}
{{ render_list_ul(list_item) }}
{% else %}
<li>{{ list_item }}</li>
{% endif %}
{% endfor %}
</ul>

View File

@@ -1,3 +1,3 @@
<p class="mt-xs mx-s">
<p class="mt-xs">
{{ paragraph_inner_html }}
</p>

View File

@@ -17,22 +17,20 @@
{{ render_paragraph(l10n("introduction.text.1", "privacy", user_lang)) }}
{{ render_paragraph(l10n("introduction.text.2", "privacy", user_lang) +
'<br><i class="fad fa-globe ml-s t-size-8"></i>
<a href="https://gdpr.eu/privacy-notice/">
https://gdpr.eu/
</a><br>
<a href="https://gdpr.eu/privacy-notice/" class="ml-xs">https://gdpr.eu/</a><br>
<i class="fad fa-globe ml-s t-size-8"></i>
<a href="https://eur-lex.europa.eu/legal-content/ALL/?uri=CELEX%3A32016R0679">
https://eur-lex.europa.eu/
</a>')}}
<a href="https://eur-lex.europa.eu/legal-content/ALL/?uri=CELEX%3A32016R0679" class="ml-xs">https://eur-lex.europa.eu/</a>')}}
{{ render_h1(l10n("v2.data.title", "privacy", user_lang), "fad fa-database") }}
{{ render_paragraph(l10n("v2.data.intro.1", "privacy", user_lang) +
'<br>' + l10n("v2.data.intro.2", "privacy", user_lang)) }}
{{ render_paragraph(l10n('v2.data.private.1', "privacy", user_lang) +
'<ul><li>' + l10n('v2.data.private_list.1', "privacy", user_lang) +
'</li><li>' + l10n('v2.data.private_list.2', "privacy", user_lang) +
'</li></ul>') }}
render_list_ul([
l10n('v2.data.private_list.1', "privacy", user_lang),
l10n('v2.data.private_list.2', "privacy", user_lang),
])
) }}
{{ render_paragraph(l10n('v2.data.non_private.1', "privacy", user_lang) +
'<ul><li>' + l10n('v2.data.non_private_list.1', "privacy", user_lang) +
'</li><li>' + l10n('v2.data.non_private_list.2', "privacy", user_lang) +
@@ -167,17 +165,17 @@
{{ render_h1(l10n("contact.title", "privacy", user_lang), "fad fa-mailbox") }}
{{ render_paragraph(l10n("contact.text.1", "privacy", user_lang) +
'<br><i class="fad fa-at t-size-8 ml-s"></i><a href="mailto:herwin.bozet@gmail.com">herwin.bozet@gmail.com</a>') }}
'<br><i class="fad fa-at t-size-8 ml-s"></i><a class="ml-xs" href="mailto:herwin.bozet@gmail.com">herwin.bozet@gmail.com</a>') }}
{{ render_h1(l10n("complaint.title", "privacy", user_lang), "fad fa-gavel") }}
{{ render_paragraph(l10n("complaint.text.1", "privacy", user_lang)) }}
{{ render_paragraph(l10n('complaint.text.2', "privacy", user_lang) +
'<br><i class="fad fa-globe ml-s t-size-8"></i>' +
'<a href="https://ec.europa.eu/info/law/law-topic/data-protection/reform/rights-citizens/redress/what-should-i-do-if-i-think-my-personal-data-protection-rights-havent-been-respected_en">https://ec.europa.eu/</a>' +
'<a class="ml-xs" href="https://ec.europa.eu/info/law/law-topic/data-protection/reform/rights-citizens/redress/what-should-i-do-if-i-think-my-personal-data-protection-rights-havent-been-respected_en">https://ec.europa.eu/</a>' +
'<span class="ml-s">(' + l10n('english', "langs", user_lang) + ')</span>' +
'<br><i class="fad fa-globe ml-s t-size-8"></i>' +
'<a href="https://gegevensbeschermingsautoriteit.be/citoyen/agir/introduire-une-plainte">https://gegevensbeschermingsautoriteit.be/</a>' +
'<a class="ml-xs" href="https://gegevensbeschermingsautoriteit.be/citoyen/agir/introduire-une-plainte">https://gegevensbeschermingsautoriteit.be/</a>' +
'<span class="ml-s">(' + l10n('french', "langs", user_lang) + ')</span>' ) }}
{% endblock %}

View File

@@ -20,34 +20,35 @@
{{ render_h2(l10n("updates.title", "home", user_lang)) }}
<p><i class="fad fa-calendar-alt mr-xs"></i>{{ l10n("updates.4.date", "home", user_lang) }}</p>
<ul>
<li>{{ l10n("updates.4.text.1", "home", user_lang) }}</li>
<li>{{ l10n("updates.4.text.2", "home", user_lang) }}</li>
<li>{{ l10n("updates.4.text.3", "home", user_lang) }}</li>
<li>{{ l10n("updates.text.privacy", "home", user_lang) }}</li>
</ul>
<p><i class="fad fa-calendar-alt mr-xs mt-s"></i>{{ l10n("updates.4.date", "home", user_lang) }}</p>
{{ render_list_ul([
l10n("updates.4.text.1", "home", user_lang),
l10n("updates.4.text.2", "home", user_lang),
l10n("updates.4.text.3", "home", user_lang),
l10n("updates.text.privacy", "home", user_lang)
]) }}
<p><i class="fad fa-calendar-alt mr-xs"></i>{{ l10n("updates.3.date", "home", user_lang) }}</p>
<ul>
<li>{{ l10n("updates.3.text.1", "home", user_lang) }}</li>
<li>{{ l10n("updates.3.text.2", "home", user_lang) }}</li>
<li>{{ l10n("updates.3.text.3", "home", user_lang) }}</li>
<li>{{ l10n("updates.text.privacy", "home", user_lang) }}</li>
</ul>
<p><i class="fad fa-calendar-alt mr-xs mt-s"></i>{{ l10n("updates.3.date", "home", user_lang) }}</p>
{{ render_list_ul([
l10n("updates.3.text.1", "home", user_lang),
l10n("updates.3.text.2", "home", user_lang),
l10n("updates.3.text.3", "home", user_lang),
l10n("updates.text.privacy", "home", user_lang)
]) }}
<p><i class="fad fa-calendar-alt mr-xs"></i>{{ l10n("updates.2.date", "home", user_lang) }}</p>
<ul>
<li>{{ l10n("updates.2.text.1", "home", user_lang) }}</li>
<li>{{ l10n("updates.2.text.2", "home", user_lang) }}</li>
<li>{{ l10n("updates.2.text.3", "home", user_lang) }}</li>
<li>{{ l10n("updates.2.text.4", "home", user_lang) }}</li>
<li>{{ l10n("updates.text.privacy", "home", user_lang) }}</li>
</ul>
<p><i class="fad fa-calendar-alt mr-xs mt-s"></i>{{ l10n("updates.2.date", "home", user_lang) }}</p>
{{ render_list_ul([
l10n("updates.2.text.1", "home", user_lang),
l10n("updates.2.text.2", "home", user_lang),
l10n("updates.2.text.3", "home", user_lang),
l10n("updates.2.text.4", "home", user_lang),
l10n("updates.text.privacy", "home", user_lang)
]) }}
<p><i class="fad fa-calendar-alt mr-xs mt-s"></i>{{ l10n("updates.1.date", "home", user_lang) }}</p>
{{ render_list_ul([
l10n("updates.1.text.1", "home", user_lang),
l10n("updates.text.privacy", "home", user_lang)
]) }}
<p><i class="fad fa-calendar-alt mr-xs"></i>{{ l10n("updates.1.date", "home", user_lang) }}</p>
<ul>
<li>{{ l10n("updates.1.text.1", "home", user_lang) }}</li>
<li>{{ l10n("updates.text.privacy", "home", user_lang) }}</li>
</ul>
{% endblock %}

View File

@@ -4,9 +4,7 @@
{% block head_description %}{{ l10n(tool_data.metadata.head.description_key, tool_id, user_lang) }}{% endblock %}
{% block extra_stylesheets %}
{% for tool_stylesheet in tool_data.data.stylesheets %}
<link rel="stylesheet" href="{{ url_for("static", filename="/resources/NibblePoker/tools/" + tool_id + "/" + tool_stylesheet) }}">
{% endfor %}
{{ render_applet_head(applet_data) }}
{% endblock %}
{% block header_title %}
@@ -21,16 +19,10 @@
) }}
<div class="px-xxs">
{% block tool_content %}{% endblock %}
{% include 'applets/'+applet_data.id+'.jinja' %}
</div>
{% endblock %}
{% block extra_scripts %}
{% for tool_script in tool_data.data.scripts %}
{% if tool_script.endswith(".mjs") %}
<script src="{{ url_for("static", filename="/resources/NibblePoker/tools/" + tool_id + "/" + tool_script) }}" type="module"></script>
{% else %}
<script src="{{ url_for("static", filename="/resources/NibblePoker/tools/" + tool_id + "/" + tool_script) }}"></script>
{% endif %}
{% endfor %}
{{ render_applet_scripts(applet_data) }}
{% endblock %}

View File

@@ -38,4 +38,5 @@
<script src="https://cdn.nibblepoker.lu/HighlightJS/11.9.0-custom/highlight.min.js"></script>
<script src="{{ url_for("static", filename="/resources/NibblePoker/js/nibblepoker-splide.min.js") }}"></script>
<script src="{{ url_for("static", filename="/resources/NibblePoker/js/nibblepoker-code.min.js") }}"></script>
{% endblock %}

View File

@@ -0,0 +1,131 @@
{% extends "projects/_project.jinja" %}
{% block project_content %}
{{ render_h2(l10n("intro.title", project_id, user_lang)) }}
{{ render_paragraph(l10n("intro.1", project_id, user_lang)) }}
{{ render_paragraph(l10n("intro.2", project_id, user_lang)) }}
{{ render_h2(l10n("preamble.title", project_id, user_lang)) }}
{{ render_paragraph(l10n("preamble.1", project_id, user_lang)) }}
{{ render_paragraph(l10n("preamble.2", project_id, user_lang)) }}
{{ render_h2(l10n("setup.title", project_id, user_lang)) }}
{{ render_paragraph(l10n("setup.1", project_id, user_lang)) }}
{{ render_paragraph(l10n("setup.2", project_id, user_lang)) }}
{{ render_h3(l10n("setup.camera.title", project_id, user_lang)) }}
{{ render_paragraph(l10n("setup.camera.1", project_id, user_lang)) }}
{{ render_paragraph(l10n("setup.camera.2", project_id, user_lang)) }}
{{ render_h4(l10n("commons.example", project_id, user_lang)) }}
{{
render_code_block([
"nvr_stack:",
" cctv_recorder_cam1:",
" container_name: cctv-recorder-cam1",
" build:",
" context: .",
" dockerfile: Dockerfile_recorder",
" environment:",
" - TZ=Europe/Brussels",
" - \"NP_CCTV_URL=rtsp://user:password@address:554/sub-path\"",
" volumes:",
" - ./recordings/cam1:/data",
" restart: unless-stopped",
], "yaml")
}}
{{ render_paragraph(l10n("setup.camera.3", project_id, user_lang)) }}
{{ render_h3(l10n("setup.cleaner.title", project_id, user_lang)) }}
{{ render_paragraph(l10n("setup.cleaner.1", project_id, user_lang)) }}
{{ render_paragraph(l10n("setup.cleaner.2", project_id, user_lang)) }}
{{ render_h4(l10n("commons.example", project_id, user_lang)) }}
{{
render_code_block([
"nvr_stack:",
" cctv_cleaner:",
" container_name: cctv-cleaner",
" build:",
" context: .",
" dockerfile: Dockerfile_cleaner",
" environment:",
" - TZ=Europe/Brussels",
" - \"NP_MAX_FILE_AGE_HOURS=72\"",
" volumes:",
" - ./recordings:/data",
" - ./cleaner.py:/app/app.py:ro",
" restart: unless-stopped",
], "yaml")
}}
{{ render_h3(l10n("setup.web.title", project_id, user_lang)) }}
{{ render_paragraph(l10n("setup.web.1", project_id, user_lang)) }}
{{ render_paragraph(l10n("setup.web.2", project_id, user_lang)) }}
{{ render_paragraph(l10n("setup.web.3", project_id, user_lang)) }}
{{ render_h4(l10n("commons.web.vars.title", project_id, user_lang)) }}
{{ render_h4(l10n("commons.example", project_id, user_lang)) }}
{{
render_code_block([
"nvr_stack:",
" cctv_web:",
" container_name: cctv-web",
" image: php:apache",
" ports:",
" - 26880:80",
" environment:",
" - TZ=Europe/Brussels",
" - \"NP_CAM_cam1=Camera #1\"",
" - \"NP_CAM_cam2=Camera #2\"",
" - \"NP_TITLE=NibblePoker's Mini CCTV NVR\"",
" - \"NP_FOOTER=Made by <i>BOZET Herwin</i>\"",
" volumes:",
" - ./htdocs:/var/www/html # Cannot be \":ro\" since the recordings are mounted into it.",
" - ./apache2.conf:/etc/apache2/apache2.conf:ro",
" - ./recordings:/var/www/html/data:ro",
" restart: unless-stopped",
], "yaml")
}}
{{ render_h2(l10n("startup.title", project_id, user_lang)) }}
{{ render_paragraph(l10n("startup.1", project_id, user_lang)) }}
{{
render_code_block([
"docker-compose up --build -d",
], "bash")
}}
{{ render_h2(l10n("screenshots.title", project_id, user_lang)) }}
{{ render_splide([
'<img src="' + url_for("static", filename="/resources/NibblePoker/images/content/" + project_id + "/home.png") + '">',
'<img src="' + url_for("static", filename="/resources/NibblePoker/images/content/" + project_id + "/cam.png") + '">',
]) }}
{{ render_h2(l10n("statistics.title", project_id, user_lang)) }}
{{ render_list_ul([
l10n("statistics.1", project_id, user_lang),
[
l10n("statistics.1.1", project_id, user_lang),
],
l10n("statistics.2", project_id, user_lang),
[
l10n("statistics.2.1", project_id, user_lang),
l10n("statistics.2.2", project_id, user_lang),
[
l10n("statistics.2.2.1", project_id, user_lang),
],
],
l10n("statistics.3", project_id, user_lang),
[
l10n("statistics.3.1", project_id, user_lang),
l10n("statistics.3.2", project_id, user_lang),
l10n("statistics.3.3", project_id, user_lang),
],
l10n("statistics.4", project_id, user_lang),
[
l10n("statistics.4.1", project_id, user_lang),
l10n("statistics.4.2", project_id, user_lang),
],
]) }}
{{ render_h2(l10n("license.title", project_id, user_lang)) }}
{{ render_paragraph(l10n("license.1", project_id, user_lang)) }}
{% endblock %}

View File

@@ -1,38 +0,0 @@
{% extends "tools/_tool.jinja" %}
{% block tool_content %}
<div class="px-xs mt-s">
<label for="uuid-generator-option-type" class="mr-xs">{{ l10n("type.label", tool_id, user_lang) }}:</label>
<select name="uuid-generator-option-type" id="uuid-generator-option-type" class="p-xxs border r-s">
<option value="type-uuid4" selected>{{ l10n("type.uuid4", tool_id, user_lang) }}</option>
<option value="type-guid">{{ l10n("type.guid", tool_id, user_lang) }}</option>
</select>
<br>
<label for="uuid-generator-option-count" class="mr-xs">{{ l10n("option.count", tool_id, user_lang) }}:</label>
<input id="uuid-generator-option-count" class="p-xxs border r-s" type="number" value="4" min="1" max="1000">
<br>
<label for="uuid-generator-option-hyphens" class="mr-xxs">{{ l10n("option.hyphen", tool_id, user_lang) }}:</label>
<input id="uuid-generator-option-hyphens" type="checkbox" checked>
<hr class="subtle">
<button id="uuid-generator-generate" class="p-xs r-s border b-light success">
<i class="fa-duotone fa-solid fa-gears mr-xs"></i>{{ l10n("generate", tool_id, user_lang) }}
</button>
<button id="uuid-generator-download" class="p-xs r-s border b-light primary">
<i class="fa-duotone fa-solid fa-download mr-xs"></i>{{ l10n("download", tool_id, user_lang) }}
</button>
<hr class="subtle">
<label for="uuid-generator-preview" class="d-none">{{ l10n("preview.label", tool_id, user_lang) }}:</label>
<textarea name="uuid-generator-preview" id="uuid-generator-preview" rows="16" class="w-full border r-s"></textarea>
</div>
{% endblock %}

View File

@@ -5,31 +5,52 @@ from typing import Any
from locked_dict.locked_dict import LockedDict
import yaml
from .metadata import ContentMetadata
from .project import ContentProject
from .tool import ContentTool, ContentToolData
from .dataclasses import *
__CONTENT: ContentRoot = ContentRoot()
__CONTENT_APPLETS: LockedDict[str, ContentApplet] = LockedDict()
__CONTENT_ARTICLES: LockedDict = LockedDict()
__CONTENT_PROJECTS: LockedDict[str, ContentProject] = LockedDict()
__CONTENT_TOOLS: LockedDict[str, ContentTool] = LockedDict()
def get_content() -> ContentRoot:
return __CONTENT
def get_applets() -> LockedDict[str, ContentApplet]:
return __CONTENT.applets
def get_articles() -> LockedDict:
return __CONTENT_ARTICLES
def get_projects() -> LockedDict[str, ContentProject]:
return __CONTENT_PROJECTS
return __CONTENT.projects
def get_projects_by_tags(tags: list[str]) -> dict[Any, ContentProject]:
project_obj: ContentProject
return {
project_key: project_value for project_key, project_value in __CONTENT_PROJECTS.items()
project_key: project_value for project_key, project_value in __CONTENT.projects.items()
if any(tag in project_value.metadata.general.tags for tag in tags)
}
def get_tools() -> LockedDict[str, ContentTool]:
return __CONTENT.tools
def get_tools_by_tags(tags: list[str]) -> dict[Any, ContentProject]:
tool_obj: ContentProject
return {
tool_key: tool_value for tool_key, tool_value in __CONTENT.tools.items()
if any(tag in tool_value.metadata.general.tags for tag in tags)
}
def sanitize_input_tags(input_tags: str) -> list[str]:
tags: list[str] = input_tags.split(";")
for tag in tags:
@@ -38,34 +59,50 @@ def sanitize_input_tags(input_tags: str) -> list[str]:
return tags
def get_tools() -> LockedDict:
return __CONTENT_TOOLS
def load_content_items() -> None:
global __CONTENT
__CONTENT = ContentRoot()
def get_tools_by_tags(tags: list[str]) -> dict[Any, ContentProject]:
tool_obj: ContentProject
return {
tool_key: tool_value for tool_key, tool_value in __CONTENT_TOOLS.items()
if any(tag in tool_value.metadata.general.tags for tag in tags)
}
# Loading applets definition files
for applets_file in os.listdir(os.path.join(os.getcwd(), "data/applets")):
applets_file_path = os.path.join(os.getcwd(), "data/applets", applets_file)
if not os.path.isfile(applets_file_path) or applets_file.startswith("."):
continue
applets_data = yaml.safe_load(open(applets_file_path))
if "applets" not in applets_data:
print(f"Unable to load '{applets_file_path}' due to missing 'applets' field !")
continue
def reload_content_items() -> None:
global __CONTENT_ARTICLES
global __CONTENT_PROJECTS
global __CONTENT_TOOLS
for applet_data in applets_data["applets"]:
_applet = ContentApplet(**applet_data)
__CONTENT.applets[_applet.id] = _applet
__CONTENT_ARTICLES = LockedDict()
__CONTENT_PROJECTS = LockedDict()
__CONTENT_TOOLS = LockedDict()
for article_folder in os.listdir(os.path.join(os.getcwd(), "data/articles")):
# Loading articles definition files
"""for article_folder in os.listdir(os.path.join(os.getcwd(), "data/articles")):
article_folder_path = os.path.join(os.getcwd(), "data/articles", article_folder)
if not os.path.isdir(article_folder_path):
continue
pass
pass"""
for project_item in os.listdir(os.path.join(os.getcwd(), "data/projects")):
# Loading projects definition files
for project_file in os.listdir(os.path.join(os.getcwd(), "data/projects")):
project_file_path = os.path.join(os.getcwd(), "data/projects", project_file)
if not os.path.isfile(project_file_path) or project_file.startswith("."):
continue
projects_data = yaml.safe_load(open(project_file_path))
if "projects" not in projects_data:
print(f"Unable to load '{project_file_path}' due to missing 'projects' field !")
continue
for project_data in projects_data["projects"]:
_project = ContentProject(**project_data)
__CONTENT.projects[_project.id] = _project
print(_project)
"""for project_item in os.listdir(os.path.join(os.getcwd(), "data/projects")):
project_item_path = os.path.join(os.getcwd(), "data/projects/", project_item)
if not os.path.isfile(project_item_path) or project_item.startswith("."):
continue
@@ -87,33 +124,25 @@ def reload_content_items() -> None:
print(f"Loaded project '{project_id}'")
except Exception as e:
print(f"Unable to load project '{project_id}' due to an exception !")
print(e)
print(e)"""
for tool_item in os.listdir(os.path.join(os.getcwd(), "data/tools")):
tool_item_path = os.path.join(os.getcwd(), "data/tools", tool_item)
if not os.path.isfile(tool_item_path) or tool_item_path.startswith("."):
for tools_file in os.listdir(os.path.join(os.getcwd(), "data/tools")):
tools_file_path = os.path.join(os.getcwd(), "data/tools", tools_file)
if not os.path.isfile(tools_file_path) or tools_file.startswith("."):
continue
tool_id = Path(tool_item_path).stem
tool_page_path = os.path.join(os.getcwd(), f"templates/tools/{tool_id}.jinja")
if not all(os.path.isfile(project_file) for project_file in
[tool_item_path, tool_page_path]):
print(f"Unable to load tool '{tool_id}' due to missing files !")
tools_data = yaml.safe_load(open(tools_file_path))
if "tools" not in tools_data:
print(f"Unable to load '{tools_file_path}' due to missing 'tools' field !")
continue
tool_data: ContentTool
try:
raw_tool_data = yaml.safe_load(open(tool_item_path))
__CONTENT_TOOLS[tool_id] = ContentTool(
id=tool_id,
metadata=ContentMetadata(**raw_tool_data["metadata"]),
data=ContentToolData(**raw_tool_data["data"]),
)
print(f"Loaded tool '{tool_id}'")
except Exception as e:
print(f"Unable to load tool '{tool_id}' due to an exception !")
print(e)
continue
for tool_data in tools_data["tools"]:
_tool = ContentTool(**tool_data)
__CONTENT.tools[_tool.id] = _tool
#print(_tool)
# FIXME: Check if the required files exist too !
# FIXME: Check if the required files exist too !"""
def validate_content_items() -> bool:
pass

View File

@@ -1,6 +1,8 @@
from dataclasses import dataclass, field
from typing import Optional
from locked_dict.locked_dict import LockedDict
@dataclass
class ContentHeadMetadata:
@@ -66,3 +68,49 @@ class ContentMetadata:
self.general: dict
self.general = ContentGeneralMetadata(**self.general)
@dataclass
class ContentResource:
scripts: list[str] = field(default_factory=list)
stylesheets: list[str] = field(default_factory=list)
@dataclass
class ContentApplet:
id: str
resources: ContentResource
def __post_init__(self):
self.resources: dict
self.resources = ContentResource(**self.resources)
@dataclass
class ContentTool:
# NOTE: could extend `ContentProject`
id: str
applet_id: str
metadata: ContentMetadata
def __post_init__(self):
self.metadata: dict
self.metadata = ContentMetadata(**self.metadata)
@dataclass
class ContentProject:
id: str
metadata: ContentMetadata
def __post_init__(self):
self.metadata: dict
self.metadata = ContentMetadata(**self.metadata)
@dataclass
class ContentRoot:
applets: LockedDict[str, ContentApplet] = field(default_factory=LockedDict)
# articles: list[Con] = field(default_factory=list)
projects: LockedDict[str, ContentProject] = field(default_factory=LockedDict)
tools: LockedDict[str, ContentTool] = field(default_factory=LockedDict)

View File

@@ -1,9 +0,0 @@
from dataclasses import dataclass
from .metadata import ContentMetadata
@dataclass
class ContentProject:
id: str
metadata: ContentMetadata

View File

@@ -1,16 +0,0 @@
from dataclasses import dataclass, field
from .metadata import ContentMetadata
@dataclass
class ContentToolData:
scripts: list[str] = field(default_factory=list)
stylesheets: list[str] = field(default_factory=list)
@dataclass
class ContentTool:
id: str
metadata: ContentMetadata
data: ContentToolData

View File

@@ -0,0 +1,29 @@
from flask import url_for
from website.content import ContentApplet
def render_applet_head(applet_data: ContentApplet) -> str:
applet_style_html = ""
for applet_style in applet_data.resources.stylesheets:
applet_style_html += ("<link rel='stylesheet' href='" +
url_for(
"static",
filename="/resources/NibblePoker/applets/" + applet_data.id + "/" + applet_style) +
"'>")
return applet_style_html
def render_applet_scripts(applet_data: ContentApplet):
applet_script_html = ""
for applet_script in applet_data.resources.scripts:
applet_script_html += ("<script src='" +
url_for(
"static",
filename="/resources/NibblePoker/applets/" + applet_data.id + "/" + applet_script) +
"'" + (" type='module'" if applet_script.endswith(".mjs") else "") + "></script>")
return applet_script_html

31
website/renderers/code.py Normal file
View File

@@ -0,0 +1,31 @@
import html
import re
from typing import Optional
from flask import render_template
def render_code_block(code_lines: list[str], language: Optional[str] = None):
_code_lines = list()
for code_line in code_lines:
code_line = html.escape(code_line)
code_line = code_line.replace('\t', '&nbsp;' * 4)
code_line = code_line.replace(' ', '&nbsp;')
_code_lines.append(code_line)
return render_template(
"elements/code.jinja",
code_lines=_code_lines,
code_language=language,
)
# return re.sub('>\s*<span', "><span",
# re.sub('<br>\s*</code>', "</code>",
# render_template(
# "elements/code.jinja",
# code_lines=_code_lines,
# code_language=language,
# )
# )
# )

View File

@@ -0,0 +1,3 @@
def render_file_input() -> str:
return ""

View File

@@ -35,3 +35,11 @@ def render_h3(inner_html: str, icon: Optional[str] = None, right_html: Optional[
return render_heading(
inner_html, 3, icon, right_html, anchor_id, background_class
)
def render_h4(inner_html: str, icon: Optional[str] = None, right_html: Optional[str] = None,
anchor_id: Optional[str] = None, background_class: str = "bkgd-grid") -> str:
return render_heading(
inner_html, 4, icon, right_html, anchor_id, background_class
)

View File

@@ -0,0 +1,11 @@
from typing import Union
from flask import render_template
def render_list_ul(items: list[Union[str|list]]) -> str:
return render_template(
"elements/list-ul.jinja",
list_items=items,
render_list_ul=render_list_ul
)