This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 71 KiB |
@@ -3,8 +3,6 @@
|
|||||||
const form = document.getElementById("notes-form");
|
const form = document.getElementById("notes-form");
|
||||||
const authorInput = document.getElementById("note-author");
|
const authorInput = document.getElementById("note-author");
|
||||||
const contentInput = document.getElementById("note-content");
|
const contentInput = document.getElementById("note-content");
|
||||||
const authorFilter = document.getElementById("notes-author-filter");
|
|
||||||
let notesCache = [];
|
|
||||||
|
|
||||||
if (!wall) return;
|
if (!wall) return;
|
||||||
|
|
||||||
@@ -36,53 +34,6 @@
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normaliseAuthor(author) {
|
|
||||||
return (author || "").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function populateAuthorFilter(notes) {
|
|
||||||
if (!authorFilter) return;
|
|
||||||
|
|
||||||
const previousValue = authorFilter.value;
|
|
||||||
const authors = Array.from(
|
|
||||||
new Set(notes.map(note => normaliseAuthor(note.author_name)).filter(Boolean))
|
|
||||||
).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
|
|
||||||
|
|
||||||
authorFilter.innerHTML = '<option value="">All authors</option>';
|
|
||||||
|
|
||||||
authors.forEach(author => {
|
|
||||||
const option = document.createElement("option");
|
|
||||||
option.value = author;
|
|
||||||
option.textContent = author;
|
|
||||||
authorFilter.appendChild(option);
|
|
||||||
});
|
|
||||||
|
|
||||||
authorFilter.value = authors.includes(previousValue) ? previousValue : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderNotes() {
|
|
||||||
wall.innerHTML = "";
|
|
||||||
|
|
||||||
const selectedAuthor = authorFilter ? authorFilter.value : "";
|
|
||||||
const notes = selectedAuthor
|
|
||||||
? notesCache.filter(note => normaliseAuthor(note.author_name) === selectedAuthor)
|
|
||||||
: notesCache;
|
|
||||||
|
|
||||||
if (notesCache.length === 0) {
|
|
||||||
wall.innerHTML = "<p>No notes yet.</p>";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notes.length === 0) {
|
|
||||||
wall.innerHTML = "<p>No notes for this author.</p>";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
notes.forEach(note => {
|
|
||||||
wall.appendChild(renderNote(note));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadNotes() {
|
async function loadNotes() {
|
||||||
wall.innerHTML = "<p>Loading notes…</p>";
|
wall.innerHTML = "<p>Loading notes…</p>";
|
||||||
|
|
||||||
@@ -91,9 +42,16 @@
|
|||||||
if (!res.ok) throw new Error("Failed to fetch notes");
|
if (!res.ok) throw new Error("Failed to fetch notes");
|
||||||
|
|
||||||
const notes = await res.json();
|
const notes = await res.json();
|
||||||
notesCache = notes;
|
wall.innerHTML = "";
|
||||||
populateAuthorFilter(notesCache);
|
|
||||||
renderNotes();
|
if (notes.length === 0) {
|
||||||
|
wall.innerHTML = "<p>No notes yet.</p>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
notes.forEach(note => {
|
||||||
|
wall.appendChild(renderNote(note));
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
wall.innerHTML = "<p>Could not load notes.</p>";
|
wall.innerHTML = "<p>Could not load notes.</p>";
|
||||||
@@ -140,9 +98,5 @@
|
|||||||
form.addEventListener("submit", submitNote);
|
form.addEventListener("submit", submitNote);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authorFilter) {
|
|
||||||
authorFilter.addEventListener("change", renderNotes);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadNotes();
|
loadNotes();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -443,38 +443,6 @@ body.no-sidenotes {
|
|||||||
NOTES BOARD
|
NOTES BOARD
|
||||||
========================= */
|
========================= */
|
||||||
|
|
||||||
#notes-tools {
|
|
||||||
display: flex;
|
|
||||||
align-items: end;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 0.65rem;
|
|
||||||
margin: 1.25rem 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#notes-tools label {
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
#notes-author-filter {
|
|
||||||
min-width: min(220px, 100%);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--surface);
|
|
||||||
color: var(--fg);
|
|
||||||
font: inherit;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1.2;
|
|
||||||
padding: 0.45rem 0.65rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#notes-author-filter:focus {
|
|
||||||
outline: 2px solid color-mix(in oklab, var(--accent) 45%, transparent);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#notes-wall {
|
#notes-wall {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
@@ -490,9 +458,7 @@ body.no-sidenotes {
|
|||||||
.note {
|
.note {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 1rem 1rem 0.9rem;
|
padding: 1rem 1rem 0.9rem;
|
||||||
background: var(--surface);
|
background: --code-bg;
|
||||||
color: var(--fg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 2px 6px rgba(0, 0, 0, 0.08),
|
0 2px 6px rgba(0, 0, 0, 0.08),
|
||||||
@@ -500,9 +466,7 @@ body.no-sidenotes {
|
|||||||
transform: rotate(var(--note-tilt, 0deg));
|
transform: rotate(var(--note-tilt, 0deg));
|
||||||
transition:
|
transition:
|
||||||
transform 0.15s ease,
|
transform 0.15s ease,
|
||||||
box-shadow 0.15s ease,
|
box-shadow 0.15s ease;
|
||||||
background-color 0.2s ease,
|
|
||||||
border-color 0.2s ease;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,7 +511,7 @@ body.no-sidenotes {
|
|||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
color: var(--muted);
|
color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-author {
|
.note-author {
|
||||||
@@ -569,7 +533,7 @@ body.no-sidenotes {
|
|||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
color: var(--fg);
|
color: --fg;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
@@ -581,11 +545,6 @@ body.no-sidenotes {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
#notes-tools {
|
|
||||||
align-items: stretch;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note {
|
.note {
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
@@ -608,9 +567,7 @@ body.no-sidenotes {
|
|||||||
#notes-form {
|
#notes-form {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 420px;
|
max-width: 420px;
|
||||||
background: var(--surface);
|
background: --bg;
|
||||||
color: var(--fg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
padding: 1.25rem 1.25rem 1.1rem;
|
padding: 1.25rem 1.25rem 1.1rem;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
@@ -643,7 +600,7 @@ body.no-sidenotes {
|
|||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--muted);
|
color: #666;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,13 +613,12 @@ body.no-sidenotes {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--fg);
|
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
padding: 0.3rem 0;
|
padding: 0.3rem 0;
|
||||||
margin-bottom: 0.9rem;
|
margin-bottom: 0.9rem;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
#notes-form textarea {
|
#notes-form textarea {
|
||||||
@@ -674,12 +630,12 @@ body.no-sidenotes {
|
|||||||
#notes-form input:focus,
|
#notes-form input:focus,
|
||||||
#notes-form textarea:focus {
|
#notes-form textarea:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-bottom-color: var(--accent);
|
border-bottom-color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* placeholder */
|
/* placeholder */
|
||||||
#notes-form ::placeholder {
|
#notes-form ::placeholder {
|
||||||
color: var(--muted);
|
color: #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
@@ -695,15 +651,14 @@ body.no-sidenotes {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
border: none;
|
border: none;
|
||||||
background: var(--fg);
|
background: #222;
|
||||||
color: var(--bg);
|
color: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
#notes-form button:hover {
|
#notes-form button:hover {
|
||||||
background: var(--accent);
|
background: #000;
|
||||||
color: var(--surface);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#notes-form button:disabled {
|
#notes-form button:disabled {
|
||||||
|
|||||||
@@ -12,15 +12,14 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import cgi
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from email.parser import BytesParser
|
|
||||||
from email.policy import default as email_default_policy
|
|
||||||
from email.utils import formatdate
|
from email.utils import formatdate
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable
|
from typing import Any
|
||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
|
|
||||||
@@ -273,16 +272,12 @@ class BuildQueue:
|
|||||||
job.started_at = time.time()
|
job.started_at = time.time()
|
||||||
job.message = "Publishing site and search index."
|
job.message = "Publishing site and search index."
|
||||||
self._current = job
|
self._current = job
|
||||||
def append_log(chunk: str) -> None:
|
ok, message, log = run_build_commands()
|
||||||
with self._lock:
|
|
||||||
job.log = (job.log + chunk)[-200000:]
|
|
||||||
|
|
||||||
ok, message, log = run_build_commands(append_log)
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
job.finished_at = time.time()
|
job.finished_at = time.time()
|
||||||
job.ok = ok
|
job.ok = ok
|
||||||
job.message = message
|
job.message = message
|
||||||
job.log = log[-200000:]
|
job.log = log
|
||||||
self._recent.append(job)
|
self._recent.append(job)
|
||||||
self._recent = self._recent[-20:]
|
self._recent = self._recent[-20:]
|
||||||
self._current = None
|
self._current = None
|
||||||
@@ -648,7 +643,7 @@ def save_upload(filename: str, payload: bytes, page_path: str = "") -> dict[str,
|
|||||||
absolute_url = f"https://zainezq.com/{rel}"
|
absolute_url = f"https://zainezq.com/{rel}"
|
||||||
relative_url = relative_asset_path(page_path, rel)
|
relative_url = relative_asset_path(page_path, rel)
|
||||||
is_markdown = page_path.endswith(".md")
|
is_markdown = page_path.endswith(".md")
|
||||||
insert_text = f"" if is_markdown else f"[[{relative_url}]]"
|
insert_text = f"" if is_markdown else f"[[{relative_url}]]"
|
||||||
return {
|
return {
|
||||||
"url": absolute_url,
|
"url": absolute_url,
|
||||||
"relativeUrl": relative_url,
|
"relativeUrl": relative_url,
|
||||||
@@ -659,35 +654,7 @@ def save_upload(filename: str, payload: bytes, page_path: str = "") -> dict[str,
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def parse_upload_form(content_type: str, body: bytes) -> tuple[str, bytes, str]:
|
def run_build_commands() -> tuple[bool, str, str]:
|
||||||
if not content_type.lower().startswith("multipart/form-data"):
|
|
||||||
raise ValueError("Uploads must use multipart/form-data.")
|
|
||||||
message = BytesParser(policy=email_default_policy).parsebytes(
|
|
||||||
f"Content-Type: {content_type}\r\nMIME-Version: 1.0\r\n\r\n".encode("utf-8") + body
|
|
||||||
)
|
|
||||||
if not message.is_multipart():
|
|
||||||
raise ValueError("Upload form is not multipart.")
|
|
||||||
|
|
||||||
filename = ""
|
|
||||||
payload = b""
|
|
||||||
page_path = ""
|
|
||||||
for part in message.iter_parts():
|
|
||||||
name = part.get_param("name", header="content-disposition")
|
|
||||||
if name == "attachment":
|
|
||||||
filename = part.get_filename() or ""
|
|
||||||
payload = part.get_payload(decode=True) or b""
|
|
||||||
elif name == "pagePath":
|
|
||||||
raw_value = part.get_payload(decode=True) or b""
|
|
||||||
page_path = raw_value.decode(part.get_content_charset() or "utf-8", errors="replace")
|
|
||||||
|
|
||||||
if not filename:
|
|
||||||
raise ValueError("No attachment was uploaded.")
|
|
||||||
if not payload:
|
|
||||||
raise ValueError("Attachment is empty.")
|
|
||||||
return filename, payload, page_path
|
|
||||||
|
|
||||||
|
|
||||||
def run_build_commands(log_callback: Callable[[str], None] | None = None) -> tuple[bool, str, str]:
|
|
||||||
venv_python = ROOT / ".venv" / "bin" / "python"
|
venv_python = ROOT / ".venv" / "bin" / "python"
|
||||||
venv_pip = ROOT / ".venv" / "bin" / "pip"
|
venv_pip = ROOT / ".venv" / "bin" / "pip"
|
||||||
commands = [["emacs", "-Q", "--script", "build-site.el"]]
|
commands = [["emacs", "-Q", "--script", "build-site.el"]]
|
||||||
@@ -700,33 +667,20 @@ def run_build_commands(log_callback: Callable[[str], None] | None = None) -> tup
|
|||||||
)
|
)
|
||||||
commands.append([str(venv_python), "search-index-json.py"])
|
commands.append([str(venv_python), "search-index-json.py"])
|
||||||
combined = []
|
combined = []
|
||||||
|
|
||||||
def append_log(text: str) -> None:
|
|
||||||
combined.append(text)
|
|
||||||
if log_callback:
|
|
||||||
log_callback(text)
|
|
||||||
|
|
||||||
ok = True
|
ok = True
|
||||||
for command in commands:
|
for command in commands:
|
||||||
append_log(f"$ {' '.join(command)}\n")
|
combined.append(f"$ {' '.join(command)}\n")
|
||||||
env = os.environ.copy()
|
proc = subprocess.run(
|
||||||
env["PYTHONUNBUFFERED"] = "1"
|
|
||||||
proc = subprocess.Popen(
|
|
||||||
command,
|
command,
|
||||||
cwd=ROOT,
|
cwd=ROOT,
|
||||||
env=env,
|
|
||||||
text=True,
|
text=True,
|
||||||
bufsize=1,
|
|
||||||
stderr=subprocess.STDOUT,
|
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
)
|
)
|
||||||
assert proc.stdout is not None
|
combined.append(proc.stdout)
|
||||||
for line in proc.stdout:
|
if proc.returncode != 0:
|
||||||
append_log(line)
|
|
||||||
return_code = proc.wait()
|
|
||||||
if return_code != 0:
|
|
||||||
ok = False
|
ok = False
|
||||||
append_log(f"\nCommand exited with {return_code}.\n")
|
combined.append(f"\nCommand exited with {proc.returncode}.\n")
|
||||||
break
|
break
|
||||||
message = "Build complete. The site output and search index were regenerated." if ok else "Build failed. Check the log below."
|
message = "Build complete. The site output and search index were regenerated." if ok else "Build failed. Check the log below."
|
||||||
return ok, message, "".join(combined)
|
return ok, message, "".join(combined)
|
||||||
@@ -1104,21 +1058,9 @@ APP_HTML = r"""<!doctype html>
|
|||||||
return String(value).replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char]));
|
return String(value).replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char]));
|
||||||
}
|
}
|
||||||
|
|
||||||
function previewImageUrl(src) {
|
|
||||||
const value = String(src || "");
|
|
||||||
if (value.startsWith("https://author.zainezq.com/assets/")) {
|
|
||||||
return value.replace("https://author.zainezq.com/assets/", "https://zainezq.com/assets/");
|
|
||||||
}
|
|
||||||
const relativeAsset = value.match(/^(?:\.\.\/)+assets\/(.+)$/);
|
|
||||||
if (relativeAsset) return `https://zainezq.com/assets/${relativeAsset[1]}`;
|
|
||||||
if (value.startsWith("/assets/")) return `https://zainezq.com${value}`;
|
|
||||||
if (value.startsWith("assets/")) return `https://zainezq.com/${value}`;
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderInlineMarkdown(value) {
|
function renderInlineMarkdown(value) {
|
||||||
let out = html(value);
|
let out = html(value);
|
||||||
out = out.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_match, alt, src) => `<img src="${previewImageUrl(src)}" alt="${alt}">`);
|
out = out.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
|
||||||
out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
||||||
out = out.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
out = out.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||||||
out = out.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
out = out.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
||||||
@@ -1231,15 +1173,14 @@ APP_HTML = r"""<!doctype html>
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function refreshBuild() {
|
async function refreshBuild() {
|
||||||
const wasRunning = Boolean(state.build && state.build.running);
|
|
||||||
try {
|
try {
|
||||||
state.build = await api("/api/build");
|
state.build = await api("/api/build");
|
||||||
renderStatus();
|
renderStatus();
|
||||||
if (wasRunning && !state.build.running) await refreshPages();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
statusBox.className = "status fail";
|
statusBox.className = "status fail";
|
||||||
statusBox.textContent = `Could not refresh build status: ${err.message}`;
|
statusBox.textContent = `Could not refresh build status: ${err.message}`;
|
||||||
}
|
}
|
||||||
|
await refreshPages();
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.title.addEventListener("input", () => {
|
editor.title.addEventListener("input", () => {
|
||||||
@@ -1283,7 +1224,6 @@ APP_HTML = r"""<!doctype html>
|
|||||||
const saved = await api("/api/page", { method: "POST", body: JSON.stringify(payload) });
|
const saved = await api("/api/page", { method: "POST", body: JSON.stringify(payload) });
|
||||||
setForm(saved);
|
setForm(saved);
|
||||||
saveMessage.textContent = "Saved. Build queued.";
|
saveMessage.textContent = "Saved. Build queued.";
|
||||||
await refreshPages();
|
|
||||||
await refreshBuild();
|
await refreshBuild();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
saveMessage.textContent = err.message;
|
saveMessage.textContent = err.message;
|
||||||
@@ -1569,12 +1509,24 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
def do_POST(self) -> None:
|
def do_POST(self) -> None:
|
||||||
if self.path == "/api/upload":
|
if self.path == "/api/upload":
|
||||||
try:
|
try:
|
||||||
length = int(self.headers.get("Content-Length", "0"))
|
form = cgi.FieldStorage(
|
||||||
filename, payload, page_path = parse_upload_form(
|
fp=self.rfile,
|
||||||
self.headers.get("Content-Type", ""),
|
headers=self.headers,
|
||||||
self.rfile.read(length),
|
environ={
|
||||||
|
"REQUEST_METHOD": "POST",
|
||||||
|
"CONTENT_TYPE": self.headers.get("Content-Type", ""),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
self.send_json(save_upload(filename, payload, page_path))
|
field = form["attachment"] if "attachment" in form else None
|
||||||
|
if field is None or not getattr(field, "filename", ""):
|
||||||
|
raise ValueError("No attachment was uploaded.")
|
||||||
|
payload = field.file.read()
|
||||||
|
if not payload:
|
||||||
|
raise ValueError("Attachment is empty.")
|
||||||
|
page_path = ""
|
||||||
|
if "pagePath" in form:
|
||||||
|
page_path = str(form["pagePath"].value or "")
|
||||||
|
self.send_json(save_upload(field.filename, payload, page_path))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
|
self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
|
||||||
return
|
return
|
||||||
|
|||||||
0
blogs/blogs-list.org
Normal file → Executable file
0
blogs/blogs-list.org
Normal file → Executable file
@@ -5,13 +5,6 @@
|
|||||||
#+SLUG: notes
|
#+SLUG: notes
|
||||||
|
|
||||||
#+BEGIN_EXPORT html
|
#+BEGIN_EXPORT html
|
||||||
<div id="notes-tools" aria-label="Note filters">
|
|
||||||
<label for="notes-author-filter">Filter by author</label>
|
|
||||||
<select id="notes-author-filter">
|
|
||||||
<option value="">All authors</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="notes-wall">
|
<div id="notes-wall">
|
||||||
<p>Loading notes…</p>
|
<p>Loading notes…</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
5
lima/index.md
Normal file → Executable file
5
lima/index.md
Normal file → Executable file
@@ -9,8 +9,3 @@ I made quite a few changes. It all started when I realised that not everyone kno
|
|||||||
- I probably didn’t explain it as well, but you can create folders and files under the `lima-website` directory. The files must have a `.md` extension at the end (markdown).
|
- I probably didn’t explain it as well, but you can create folders and files under the `lima-website` directory. The files must have a `.md` extension at the end (markdown).
|
||||||
|
|
||||||
- One other thing, a more personal one: what do you think of all this? am i doing too much? am i doing too little? am i overengineering things? am i forcing you to do something you dont wanna do? these little trinkets work for me, but i’m not sure if it would work for someone else, so at any point if you have reservations, let me know okay?
|
- One other thing, a more personal one: what do you think of all this? am i doing too much? am i doing too little? am i overengineering things? am i forcing you to do something you dont wanna do? these little trinkets work for me, but i’m not sure if it would work for someone else, so at any point if you have reservations, let me know okay?
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
This is a test
|
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ See the categories: @@html:<a href="../../home/categories.html">Categories</a>@@
|
|||||||
- [[file:restful-api.org][Restful API]] @@html:<span class="post-date">15-02-2026 23:00</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:restful-api.org][Restful API]] @@html:<span class="post-date">15-02-2026 23:00</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
|
|
||||||
** January 2026
|
** January 2026
|
||||||
|
- [[file:database-permissions.org][Database Permissions, Roles, and Accounts]] @@html:<span class="post-date">18-01-2026 23:00</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
- [[file:monitoring-and-logging.org][Monitoring and Logging]] @@html:<span class="post-date">18-01-2026 23:00</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:monitoring-and-logging.org][Monitoring and Logging]] @@html:<span class="post-date">18-01-2026 23:00</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
- [[file:pipelines.org][Pipelines and how they work (as well as CI/CD)]] @@html:<span class="post-date">18-01-2026 23:00</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:pipelines.org][Pipelines and how they work (as well as CI/CD)]] @@html:<span class="post-date">18-01-2026 23:00</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
- [[file:database-permissions.org][Database Permissions, Roles, and Accounts]] @@html:<span class="post-date">18-01-2026 23:00</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
|
||||||
|
|
||||||
** December 2025
|
** December 2025
|
||||||
- [[file:probation-objectives.org][Probation Objectives:]] @@html:<span class="post-date">08-12-2025 17:55</span>@@ @@html:<a href="/tags/review.html"><span class="post-tag">review</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:probation-objectives.org][Probation Objectives:]] @@html:<span class="post-date">08-12-2025 17:55</span>@@ @@html:<a href="/tags/review.html"><span class="post-tag">review</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
@@ -23,13 +23,13 @@ See the categories: @@html:<a href="../../home/categories.html">Categories</a>@@
|
|||||||
** November 2025
|
** November 2025
|
||||||
- [[file:airflow.org][Datamarts, Airflow and DAG's]] @@html:<span class="post-date">15-11-2025 18:37</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:airflow.org][Datamarts, Airflow and DAG's]] @@html:<span class="post-date">15-11-2025 18:37</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
- [[file:normalisation.org][Benefits of Normalisation]] @@html:<span class="post-date">10-11-2025 18:04</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:normalisation.org][Benefits of Normalisation]] @@html:<span class="post-date">10-11-2025 18:04</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
- [[file:wireframe-designs.org][Wireframe Designs]] @@html:<span class="post-date">06-11-2025 22:01</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
|
||||||
- [[file:management-of-self.org][Management of self training]] @@html:<span class="post-date">06-11-2025 22:01</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:management-of-self.org][Management of self training]] @@html:<span class="post-date">06-11-2025 22:01</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
|
- [[file:wireframe-designs.org][Wireframe Designs]] @@html:<span class="post-date">06-11-2025 22:01</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
- [[file:requirements-features.org][Requirements, features, user stories, tasks, walking skeletons]] @@html:<span class="post-date">06-11-2025 22:01</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:requirements-features.org][Requirements, features, user stories, tasks, walking skeletons]] @@html:<span class="post-date">06-11-2025 22:01</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
- [[file:career-intro.org][Career Introduction]] @@html:<span class="post-date">06-11-2025 21:29</span>@@ @@html:<a href="/tags/introduction.html"><span class="post-tag">introduction</span></a>@@
|
- [[file:career-intro.org][Career Introduction]] @@html:<span class="post-date">06-11-2025 21:29</span>@@ @@html:<a href="/tags/introduction.html"><span class="post-tag">introduction</span></a>@@
|
||||||
- [[file:invest-principles.org][Invest Principles]] @@html:<span class="post-date">06-11-2025 21:08</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:invest-principles.org][Invest Principles]] @@html:<span class="post-date">06-11-2025 21:08</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
- [[file:lean.org][Lean]] @@html:<span class="post-date">05-11-2025 20:46</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
|
||||||
- [[file:retrospectives.org][Retrospectives]] @@html:<span class="post-date">05-11-2025 20:46</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:retrospectives.org][Retrospectives]] @@html:<span class="post-date">05-11-2025 20:46</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
|
- [[file:lean.org][Lean]] @@html:<span class="post-date">05-11-2025 20:46</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
|
|
||||||
** October 2025
|
** October 2025
|
||||||
- [[file:owasp.org][OWASP Top Ten]] @@html:<span class="post-date">19-10-2025 13:21</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:owasp.org][OWASP Top Ten]] @@html:<span class="post-date">19-10-2025 13:21</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
|
|||||||
@@ -4,7 +4,12 @@
|
|||||||
See the categories: @@html:<a href="../home/categories.html">Categories</a>@@
|
See the categories: @@html:<a href="../home/categories.html">Categories</a>@@
|
||||||
|
|
||||||
* Posts:
|
* Posts:
|
||||||
- [[file:career/career-list.org][Career List]] @@html:<span class="post-date">09-05-2026 16:24</span>@@
|
- [[file:career/career-list.org][Career List]] @@html:<span class="post-date">09-05-2026 01:18</span>@@
|
||||||
|
- [[file:posts-list.sync-conflict-20260509-011812-NE5VEIB.org][Posts List]] @@html:<span class="post-date">09-05-2026 01:18</span>@@
|
||||||
|
- [[file:posts-list.sync-conflict-20260509-011752-NE5VEIB.org][Posts List]] @@html:<span class="post-date">09-05-2026 01:17</span>@@
|
||||||
|
- [[file:posts-list.sync-conflict-20260509-011732-NE5VEIB.org][Posts List]] @@html:<span class="post-date">09-05-2026 01:17</span>@@
|
||||||
|
- [[file:posts-list.sync-conflict-20260509-005434-VT6366A.org][Posts List]] @@html:<span class="post-date">09-05-2026 00:54</span>@@
|
||||||
|
- [[file:posts-list.sync-conflict-20260417-233432-VT6366A.org][Posts List]] @@html:<span class="post-date">14-04-2026 16:36</span>@@
|
||||||
- [[file:career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:<span class="post-date">11-03-2026 17:18</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:<span class="post-date">11-03-2026 17:18</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
- [[file:career/javascript.org][Understands the Javascript language]] @@html:<span class="post-date">11-03-2026 16:52</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:career/javascript.org][Understands the Javascript language]] @@html:<span class="post-date">11-03-2026 16:52</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
- [[file:career/restful-api.org][Restful API]] @@html:<span class="post-date">15-02-2026 23:00</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:career/restful-api.org][Restful API]] @@html:<span class="post-date">15-02-2026 23:00</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
|
|||||||
@@ -2,13 +2,18 @@
|
|||||||
#+OPTIONS: toc:nil num:nil
|
#+OPTIONS: toc:nil num:nil
|
||||||
|
|
||||||
* Recently Updated (top 26 files)
|
* Recently Updated (top 26 files)
|
||||||
- [[file:home/notes.org][Notes]] @@html:<span class="post-date">2026-05-09 15:05</span>@@
|
- [[file:posts/posts-list.sync-conflict-20260509-011812-NE5VEIB.org][Posts List]] @@html:<span class="post-date">2026-05-09 01:18</span>@@
|
||||||
- [[file:home/wird-tracker.org][Wird Tracker]] @@html:<span class="post-date">2026-05-09 01:18</span>@@
|
- [[file:sitemap.sync-conflict-20260509-011812-NE5VEIB.org][Sitemap]] @@html:<span class="post-date">2026-05-09 01:17</span>@@
|
||||||
- [[file:home/status.org][Competency Status Board]] @@html:<span class="post-date">2026-05-09 01:18</span>@@
|
- [[file:recently-updated.sync-conflict-20260509-011802-NE5VEIB.org][Recently Updated]] @@html:<span class="post-date">2026-05-09 01:17</span>@@
|
||||||
- [[file:home/services.org][Service]] @@html:<span class="post-date">2026-05-09 01:18</span>@@
|
- [[file:posts/posts-list.sync-conflict-20260509-011752-NE5VEIB.org][Posts List]] @@html:<span class="post-date">2026-05-09 01:17</span>@@
|
||||||
- [[file:home/contact.org][Contact]] @@html:<span class="post-date">2026-05-09 01:18</span>@@
|
- [[file:sitemap.sync-conflict-20260509-011752-NE5VEIB.org][Sitemap]] @@html:<span class="post-date">2026-05-09 01:17</span>@@
|
||||||
- [[file:home/guide/wird-tracker-guide.org][Wird Tracker — Technical Guide]] @@html:<span class="post-date">2026-05-09 01:18</span>@@
|
- [[file:posts/posts-list.sync-conflict-20260509-011732-NE5VEIB.org][Posts List]] @@html:<span class="post-date">2026-05-09 01:17</span>@@
|
||||||
- [[file:home/guide/setup.org][Setup]] @@html:<span class="post-date">2026-05-09 01:18</span>@@
|
- [[file:sitemap.sync-conflict-20260509-011732-NE5VEIB.org][Sitemap]] @@html:<span class="post-date">2026-05-09 01:17</span>@@
|
||||||
|
- [[file:sitemap.sync-conflict-20260509-005434-VT6366A.org][Sitemap]] @@html:<span class="post-date">2026-05-09 00:54</span>@@
|
||||||
|
- [[file:posts/posts-list.sync-conflict-20260509-005434-VT6366A.org][Posts List]] @@html:<span class="post-date">2026-05-09 00:54</span>@@
|
||||||
|
- [[file:recently-updated.sync-conflict-20260509-005434-VT6366A.org][Recently Updated]] @@html:<span class="post-date">2026-05-09 00:54</span>@@
|
||||||
|
- [[file:home/login.org][Login]] @@html:<span class="post-date">2026-05-09 00:46</span>@@
|
||||||
|
- [[file:home/guide/setup.org][Setup]] @@html:<span class="post-date">2026-05-09 00:33</span>@@
|
||||||
- [[file:blogs/2026/05-may/ai-datacamp-08-05.org][AI Datacamp]] @@html:<span class="post-date">2026-05-08 12:49</span>@@
|
- [[file:blogs/2026/05-may/ai-datacamp-08-05.org][AI Datacamp]] @@html:<span class="post-date">2026-05-08 12:49</span>@@
|
||||||
- [[file:blogs/2026/05-may/using-codex-07-05-26.org][Using Codex]] @@html:<span class="post-date">2026-05-07 16:12</span>@@
|
- [[file:blogs/2026/05-may/using-codex-07-05-26.org][Using Codex]] @@html:<span class="post-date">2026-05-07 16:12</span>@@
|
||||||
- [[file:blogs/2026/05-may/03-05-week-review.org][[03-05-2026] - Weekly Review]] @@html:<span class="post-date">2026-05-03 12:00</span>@@
|
- [[file:blogs/2026/05-may/03-05-week-review.org][[03-05-2026] - Weekly Review]] @@html:<span class="post-date">2026-05-03 12:00</span>@@
|
||||||
@@ -17,14 +22,9 @@
|
|||||||
- [[file:blogs/2026/04-april/april-almost-over-28-04.org][April is almost over...]] @@html:<span class="post-date">2026-04-28 15:33</span>@@
|
- [[file:blogs/2026/04-april/april-almost-over-28-04.org][April is almost over...]] @@html:<span class="post-date">2026-04-28 15:33</span>@@
|
||||||
- [[file:blogs/2026/04-april/26-04-week-review.org][[26-04-2026] - Weekly Review]] @@html:<span class="post-date">2026-04-26 12:00</span>@@
|
- [[file:blogs/2026/04-april/26-04-week-review.org][[26-04-2026] - Weekly Review]] @@html:<span class="post-date">2026-04-26 12:00</span>@@
|
||||||
- [[file:blogs/2026/04-april/adding-more-wird-22-04-26.org][Adding another Wird]] @@html:<span class="post-date">2026-04-22 16:04</span>@@
|
- [[file:blogs/2026/04-april/adding-more-wird-22-04-26.org][Adding another Wird]] @@html:<span class="post-date">2026-04-22 16:04</span>@@
|
||||||
|
- [[file:home/wird-tracker.org][Wird Tracker]] @@html:<span class="post-date">2026-04-22 11:58</span>@@
|
||||||
- [[file:blogs/2026/04-april/sitting-outside-in-the-sun-21-04-26.org][Sitting in the sun]] @@html:<span class="post-date">2026-04-21 13:01</span>@@
|
- [[file:blogs/2026/04-april/sitting-outside-in-the-sun-21-04-26.org][Sitting in the sun]] @@html:<span class="post-date">2026-04-21 13:01</span>@@
|
||||||
- [[file:blogs/2026/04-april/19-04-week-review.org][[19-04-2026] - Weekly Review]] @@html:<span class="post-date">2026-04-19 12:00</span>@@
|
- [[file:blogs/2026/04-april/19-04-week-review.org][[19-04-2026] - Weekly Review]] @@html:<span class="post-date">2026-04-19 12:00</span>@@
|
||||||
- [[file:blogs/2026/04-april/ending-the-week-17-04-26.org][End of week thoughts]] @@html:<span class="post-date">2026-04-17 16:17</span>@@
|
- [[file:blogs/2026/04-april/ending-the-week-17-04-26.org][End of week thoughts]] @@html:<span class="post-date">2026-04-17 16:17</span>@@
|
||||||
- [[file:blogs/2026/04-april/16-04-26.org][Rambles]] @@html:<span class="post-date">2026-04-16 16:02</span>@@
|
- [[file:blogs/2026/04-april/16-04-26.org][Rambles]] @@html:<span class="post-date">2026-04-16 16:02</span>@@
|
||||||
- [[file:blogs/2026/04-april/comparison-14-04.org][Comparison is the thief of joy]] @@html:<span class="post-date">2026-04-14 09:55</span>@@
|
- [[file:posts/posts-list.sync-conflict-20260417-233432-VT6366A.org][Posts List]] @@html:<span class="post-date">2026-04-14 16:36</span>@@
|
||||||
- [[file:blogs/2026/04-april/12-04-week-review.org][[12-04-2026] - Weekly Review]] @@html:<span class="post-date">2026-04-12 12:00</span>@@
|
|
||||||
- [[file:blogs/2026/04-april/starting-new-rotation-08-04-26.org][Starting new rotation]] @@html:<span class="post-date">2026-04-08 16:15</span>@@
|
|
||||||
- [[file:blogs/2026/04-april/action-plan-07-04-26.org][Action plan to change teams]] @@html:<span class="post-date">2026-04-07 10:04</span>@@
|
|
||||||
- [[file:blogs/2026/04-april/weekly-target-06-04-26.org][[06-04-2026] - Weekly target]] @@html:<span class="post-date">2026-04-06 22:53</span>@@
|
|
||||||
- [[file:blogs/2026/04-april/making-notes-through-org-noter-06-05.org][Making Notes through Org Noter]] @@html:<span class="post-date">2026-04-06 14:30</span>@@
|
|
||||||
- [[file:blogs/2026/04-april/05-04-week-review.org][[05-04-2026] - Weekly Review]] @@html:<span class="post-date">2026-04-05 12:00</span>@@
|
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
lxml
|
lxml
|
||||||
legacy-cgi
|
|
||||||
|
|||||||
24
sitemap.org
24
sitemap.org
@@ -1,11 +1,17 @@
|
|||||||
#+TITLE: Sitemap
|
#+TITLE: Sitemap
|
||||||
|
|
||||||
- [[file:index.org][Home]]
|
- [[file:index.org][Home]]
|
||||||
|
- [[file:recently-updated.sync-conflict-20260509-005434-VT6366A.org][Recently Updated]]
|
||||||
|
- [[file:sitemap.sync-conflict-20260509-005434-VT6366A.org][Sitemap]]
|
||||||
|
- [[file:sitemap.sync-conflict-20260509-011732-NE5VEIB.org][Sitemap]]
|
||||||
|
- [[file:sitemap.sync-conflict-20260509-011752-NE5VEIB.org][Sitemap]]
|
||||||
|
- [[file:recently-updated.sync-conflict-20260509-011802-NE5VEIB.org][Recently Updated]]
|
||||||
|
- [[file:sitemap.sync-conflict-20260509-011812-NE5VEIB.org][Sitemap]]
|
||||||
- [[file:wip.org][Work in progress]]
|
- [[file:wip.org][Work in progress]]
|
||||||
- [[file:recently-updated.org][Recently Updated]]
|
- [[file:recently-updated.org][Recently Updated]]
|
||||||
- tags
|
- tags
|
||||||
- [[file:tags/life.sync-conflict-20260417-233423-VT6366A.org][Tag: life]]
|
|
||||||
- [[file:tags/review.sync-conflict-20260328-203248-VT6366A.org][Tag: review]]
|
- [[file:tags/review.sync-conflict-20260328-203248-VT6366A.org][Tag: review]]
|
||||||
|
- [[file:tags/life.sync-conflict-20260417-233423-VT6366A.org][Tag: life]]
|
||||||
- [[file:tags/introduction.org][Tag: introduction]]
|
- [[file:tags/introduction.org][Tag: introduction]]
|
||||||
- [[file:tags/learning.org][Tag: learning]]
|
- [[file:tags/learning.org][Tag: learning]]
|
||||||
- [[file:tags/notes.org][Tag: notes]]
|
- [[file:tags/notes.org][Tag: notes]]
|
||||||
@@ -15,9 +21,9 @@
|
|||||||
- [[file:tags/life.org][Tag: life]]
|
- [[file:tags/life.org][Tag: life]]
|
||||||
- [[file:tags/education.org][Tag: education]]
|
- [[file:tags/education.org][Tag: education]]
|
||||||
- [[file:tags/insights.org][Tag: insights]]
|
- [[file:tags/insights.org][Tag: insights]]
|
||||||
- [[file:tags/reading.org][Tag: reading]]
|
|
||||||
- [[file:tags/emacs.org][Tag: emacs]]
|
- [[file:tags/emacs.org][Tag: emacs]]
|
||||||
- [[file:tags/maths.org][Tag: maths]]
|
- [[file:tags/maths.org][Tag: maths]]
|
||||||
|
- [[file:tags/reading.org][Tag: reading]]
|
||||||
- books
|
- books
|
||||||
- [[file:books/books-list.org][Books List]]
|
- [[file:books/books-list.org][Books List]]
|
||||||
- clean-code
|
- clean-code
|
||||||
@@ -26,22 +32,28 @@
|
|||||||
- [[file:lima/lima-list.org][Lima]]
|
- [[file:lima/lima-list.org][Lima]]
|
||||||
- home
|
- home
|
||||||
- [[file:home/countdown.org][Countdown]]
|
- [[file:home/countdown.org][Countdown]]
|
||||||
|
- [[file:home/contact.org][Contact]]
|
||||||
- [[file:home/backlog.org][Backlog]]
|
- [[file:home/backlog.org][Backlog]]
|
||||||
|
- [[file:home/notes.org][Notes]]
|
||||||
|
- [[file:home/services.org][Service]]
|
||||||
- [[file:home/status.org][Competency Status Board]]
|
- [[file:home/status.org][Competency Status Board]]
|
||||||
- [[file:home/wird-tracker.org][Wird Tracker]]
|
- [[file:home/wird-tracker.org][Wird Tracker]]
|
||||||
- [[file:home/contact.org][Contact]]
|
- [[file:home/login.org][Login]]
|
||||||
- [[file:home/services.org][Service]]
|
|
||||||
- [[file:home/notes.org][Notes]]
|
|
||||||
- [[file:home/categories.org][Categories]]
|
- [[file:home/categories.org][Categories]]
|
||||||
- guide
|
- guide
|
||||||
- [[file:home/guide/setup.org][Setup]]
|
|
||||||
- [[file:home/guide/wird-tracker-guide.org][Wird Tracker — Technical Guide]]
|
- [[file:home/guide/wird-tracker-guide.org][Wird Tracker — Technical Guide]]
|
||||||
|
- [[file:home/guide/setup.org][Setup]]
|
||||||
- blogs
|
- blogs
|
||||||
- [[file:blogs/blogs-intro.org][Blogs Introduction]]
|
- [[file:blogs/blogs-intro.org][Blogs Introduction]]
|
||||||
- [[file:blogs/publish-pages.org][How to publish pages using Org Publish]]
|
- [[file:blogs/publish-pages.org][How to publish pages using Org Publish]]
|
||||||
- [[file:blogs/blogs-list.org][Blogs List]]
|
- [[file:blogs/blogs-list.org][Blogs List]]
|
||||||
- posts
|
- posts
|
||||||
- [[file:posts/posts-intro.org][Posts Introduction]]
|
- [[file:posts/posts-intro.org][Posts Introduction]]
|
||||||
|
- [[file:posts/posts-list.sync-conflict-20260417-233432-VT6366A.org][Posts List]]
|
||||||
|
- [[file:posts/posts-list.sync-conflict-20260509-005434-VT6366A.org][Posts List]]
|
||||||
|
- [[file:posts/posts-list.sync-conflict-20260509-011732-NE5VEIB.org][Posts List]]
|
||||||
|
- [[file:posts/posts-list.sync-conflict-20260509-011752-NE5VEIB.org][Posts List]]
|
||||||
|
- [[file:posts/posts-list.sync-conflict-20260509-011812-NE5VEIB.org][Posts List]]
|
||||||
- [[file:posts/posts-list.org][Posts List]]
|
- [[file:posts/posts-list.org][Posts List]]
|
||||||
- career
|
- career
|
||||||
- [[file:posts/career/solid-principles.org][SOLID Principles]]
|
- [[file:posts/career/solid-principles.org][SOLID Principles]]
|
||||||
|
|||||||
@@ -5,16 +5,16 @@
|
|||||||
- [[file:../posts/career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]]
|
- [[file:../posts/career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]]
|
||||||
- [[file:../posts/career/javascript.org][Understands the Javascript language]]
|
- [[file:../posts/career/javascript.org][Understands the Javascript language]]
|
||||||
- [[file:../posts/career/restful-api.org][Restful API]]
|
- [[file:../posts/career/restful-api.org][Restful API]]
|
||||||
|
- [[file:../posts/career/database-permissions.org][Database Permissions, Roles, and Accounts]]
|
||||||
- [[file:../posts/career/monitoring-and-logging.org][Monitoring and Logging]]
|
- [[file:../posts/career/monitoring-and-logging.org][Monitoring and Logging]]
|
||||||
- [[file:../posts/career/pipelines.org][Pipelines and how they work (as well as CI/CD)]]
|
- [[file:../posts/career/pipelines.org][Pipelines and how they work (as well as CI/CD)]]
|
||||||
- [[file:../posts/career/database-permissions.org][Database Permissions, Roles, and Accounts]]
|
|
||||||
- [[file:../posts/career/airflow.org][Datamarts, Airflow and DAG's]]
|
- [[file:../posts/career/airflow.org][Datamarts, Airflow and DAG's]]
|
||||||
- [[file:../posts/career/normalisation.org][Benefits of Normalisation]]
|
- [[file:../posts/career/normalisation.org][Benefits of Normalisation]]
|
||||||
- [[file:../posts/career/wireframe-designs.org][Wireframe Designs]]
|
|
||||||
- [[file:../posts/career/management-of-self.org][Management of self training]]
|
- [[file:../posts/career/management-of-self.org][Management of self training]]
|
||||||
|
- [[file:../posts/career/wireframe-designs.org][Wireframe Designs]]
|
||||||
- [[file:../posts/career/requirements-features.org][Requirements, features, user stories, tasks, walking skeletons]]
|
- [[file:../posts/career/requirements-features.org][Requirements, features, user stories, tasks, walking skeletons]]
|
||||||
- [[file:../posts/career/invest-principles.org][Invest Principles]]
|
- [[file:../posts/career/invest-principles.org][Invest Principles]]
|
||||||
- [[file:../posts/career/lean.org][Lean]]
|
|
||||||
- [[file:../posts/career/retrospectives.org][Retrospectives]]
|
- [[file:../posts/career/retrospectives.org][Retrospectives]]
|
||||||
|
- [[file:../posts/career/lean.org][Lean]]
|
||||||
- [[file:../posts/career/owasp.org][OWASP Top Ten]]
|
- [[file:../posts/career/owasp.org][OWASP Top Ten]]
|
||||||
- [[file:../posts/career/solid-principles.org][SOLID Principles]]
|
- [[file:../posts/career/solid-principles.org][SOLID Principles]]
|
||||||
|
|||||||
@@ -5,17 +5,17 @@
|
|||||||
- [[file:../posts/career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]]
|
- [[file:../posts/career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]]
|
||||||
- [[file:../posts/career/javascript.org][Understands the Javascript language]]
|
- [[file:../posts/career/javascript.org][Understands the Javascript language]]
|
||||||
- [[file:../posts/career/restful-api.org][Restful API]]
|
- [[file:../posts/career/restful-api.org][Restful API]]
|
||||||
|
- [[file:../posts/career/database-permissions.org][Database Permissions, Roles, and Accounts]]
|
||||||
- [[file:../posts/career/monitoring-and-logging.org][Monitoring and Logging]]
|
- [[file:../posts/career/monitoring-and-logging.org][Monitoring and Logging]]
|
||||||
- [[file:../posts/career/pipelines.org][Pipelines and how they work (as well as CI/CD)]]
|
- [[file:../posts/career/pipelines.org][Pipelines and how they work (as well as CI/CD)]]
|
||||||
- [[file:../posts/career/database-permissions.org][Database Permissions, Roles, and Accounts]]
|
|
||||||
- [[file:../posts/career/probation-objectives.org][Probation Objectives:]]
|
- [[file:../posts/career/probation-objectives.org][Probation Objectives:]]
|
||||||
- [[file:../posts/career/airflow.org][Datamarts, Airflow and DAG's]]
|
- [[file:../posts/career/airflow.org][Datamarts, Airflow and DAG's]]
|
||||||
- [[file:../posts/career/normalisation.org][Benefits of Normalisation]]
|
- [[file:../posts/career/normalisation.org][Benefits of Normalisation]]
|
||||||
- [[file:../posts/career/wireframe-designs.org][Wireframe Designs]]
|
|
||||||
- [[file:../posts/career/management-of-self.org][Management of self training]]
|
- [[file:../posts/career/management-of-self.org][Management of self training]]
|
||||||
|
- [[file:../posts/career/wireframe-designs.org][Wireframe Designs]]
|
||||||
- [[file:../posts/career/requirements-features.org][Requirements, features, user stories, tasks, walking skeletons]]
|
- [[file:../posts/career/requirements-features.org][Requirements, features, user stories, tasks, walking skeletons]]
|
||||||
- [[file:../posts/career/invest-principles.org][Invest Principles]]
|
- [[file:../posts/career/invest-principles.org][Invest Principles]]
|
||||||
- [[file:../posts/career/lean.org][Lean]]
|
|
||||||
- [[file:../posts/career/retrospectives.org][Retrospectives]]
|
- [[file:../posts/career/retrospectives.org][Retrospectives]]
|
||||||
|
- [[file:../posts/career/lean.org][Lean]]
|
||||||
- [[file:../posts/career/owasp.org][OWASP Top Ten]]
|
- [[file:../posts/career/owasp.org][OWASP Top Ten]]
|
||||||
- [[file:../posts/career/solid-principles.org][SOLID Principles]]
|
- [[file:../posts/career/solid-principles.org][SOLID Principles]]
|
||||||
|
|||||||
@@ -103,24 +103,6 @@ class UtilityTests(AuthoringServerTestCase):
|
|||||||
self.assertEqual(server.image_dimensions(gif, ".gif"), (320, 200))
|
self.assertEqual(server.image_dimensions(gif, ".gif"), (320, 200))
|
||||||
self.assertIsNone(server.image_dimensions(b"not an image", ".png"))
|
self.assertIsNone(server.image_dimensions(b"not an image", ".png"))
|
||||||
|
|
||||||
def test_parse_upload_form_reads_attachment_and_page_path(self):
|
|
||||||
boundary = "----authoring-test"
|
|
||||||
content_type = f"multipart/form-data; boundary={boundary}"
|
|
||||||
body = (
|
|
||||||
f"--{boundary}\r\n"
|
|
||||||
'Content-Disposition: form-data; name="pagePath"\r\n\r\n'
|
|
||||||
"lima/index.md\r\n"
|
|
||||||
f"--{boundary}\r\n"
|
|
||||||
'Content-Disposition: form-data; name="attachment"; filename="photo.png"\r\n'
|
|
||||||
"Content-Type: image/png\r\n\r\n"
|
|
||||||
).encode("utf-8") + b"image bytes\r\n" + f"--{boundary}--\r\n".encode("utf-8")
|
|
||||||
|
|
||||||
filename, payload, page_path = server.parse_upload_form(content_type, body)
|
|
||||||
|
|
||||||
self.assertEqual(filename, "photo.png")
|
|
||||||
self.assertEqual(payload, b"image bytes")
|
|
||||||
self.assertEqual(page_path, "lima/index.md")
|
|
||||||
|
|
||||||
|
|
||||||
class PageRenderingTests(AuthoringServerTestCase):
|
class PageRenderingTests(AuthoringServerTestCase):
|
||||||
def test_render_org_writes_metadata_and_body(self):
|
def test_render_org_writes_metadata_and_body(self):
|
||||||
@@ -262,22 +244,6 @@ class PageRenderingTests(AuthoringServerTestCase):
|
|||||||
self.assertEqual(saved["path"], "assets/images/blogs/2026-05-07-lunch.jpg")
|
self.assertEqual(saved["path"], "assets/images/blogs/2026-05-07-lunch.jpg")
|
||||||
self.assertEqual(saved["insertText"], "[[../../../assets/images/blogs/2026-05-07-lunch.jpg]]")
|
self.assertEqual(saved["insertText"], "[[../../../assets/images/blogs/2026-05-07-lunch.jpg]]")
|
||||||
|
|
||||||
def test_save_upload_inserts_absolute_public_url_for_markdown(self):
|
|
||||||
with mock.patch.object(server, "datetime") as datetime_mock:
|
|
||||||
datetime_mock.now.return_value = datetime(2026, 5, 9, 9, 30)
|
|
||||||
saved = server.save_upload(
|
|
||||||
"Screen Shot.png",
|
|
||||||
b"image bytes",
|
|
||||||
"lima/index.md",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(saved["path"], "assets/images/hzone/2026-05-09-screen-shot.png")
|
|
||||||
self.assertEqual(saved["relativeUrl"], "../assets/images/hzone/2026-05-09-screen-shot.png")
|
|
||||||
self.assertEqual(
|
|
||||||
saved["insertText"],
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_save_upload_rejects_disallowed_extensions(self):
|
def test_save_upload_rejects_disallowed_extensions(self):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
server.save_upload("shell.php", b"<?php")
|
server.save_upload("shell.php", b"<?php")
|
||||||
@@ -313,43 +279,6 @@ class BuildQueueTests(unittest.TestCase):
|
|||||||
self.assertEqual(queued["status"], "queued")
|
self.assertEqual(queued["status"], "queued")
|
||||||
self.assertEqual(queued["path"], "blogs/post.org")
|
self.assertEqual(queued["path"], "blogs/post.org")
|
||||||
|
|
||||||
def test_run_build_commands_streams_output_to_callback(self):
|
|
||||||
root = Path(tempfile.mkdtemp())
|
|
||||||
try:
|
|
||||||
venv_python = root / ".venv" / "bin" / "python"
|
|
||||||
venv_python.parent.mkdir(parents=True)
|
|
||||||
venv_python.write_text("", encoding="utf-8")
|
|
||||||
seen = []
|
|
||||||
|
|
||||||
class FakeProcess:
|
|
||||||
def __init__(self, lines, return_code=0):
|
|
||||||
self.stdout = iter(lines)
|
|
||||||
self.return_code = return_code
|
|
||||||
|
|
||||||
def wait(self):
|
|
||||||
return self.return_code
|
|
||||||
|
|
||||||
processes = [
|
|
||||||
FakeProcess(["emacs line 1\n", "emacs line 2\n"]),
|
|
||||||
FakeProcess(["index line\n"]),
|
|
||||||
]
|
|
||||||
|
|
||||||
with mock.patch.object(server, "ROOT", root), mock.patch.object(server.subprocess, "Popen", side_effect=processes):
|
|
||||||
ok, message, log = server.run_build_commands(seen.append)
|
|
||||||
|
|
||||||
self.assertTrue(ok)
|
|
||||||
self.assertIn("Build complete", message)
|
|
||||||
self.assertIn("emacs line 1\n", seen)
|
|
||||||
self.assertIn("index line\n", seen)
|
|
||||||
self.assertEqual(log, "".join(seen))
|
|
||||||
finally:
|
|
||||||
for path in sorted(root.rglob("*"), reverse=True):
|
|
||||||
if path.is_file():
|
|
||||||
path.unlink()
|
|
||||||
else:
|
|
||||||
path.rmdir()
|
|
||||||
root.rmdir()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user