diff --git a/molodetz/constants.py b/molodetz/constants.py index e9ca31c..ea3004d 100644 --- a/molodetz/constants.py +++ b/molodetz/constants.py @@ -1,4 +1,10 @@ # retoor +import re +from pathlib import Path + +INTRO_SEED_FILE = Path(__file__).resolve().parent / "seed" / "intro.md" +SITE_INTRO_SEED = re.sub(r"\A\n", "", INTRO_SEED_FILE.read_text(encoding="utf-8"), count=1, flags=re.S) + ROLE_ADMIN = "Admin" ROLE_MEMBER = "Member" ROLES = (ROLE_ADMIN, ROLE_MEMBER) @@ -76,6 +82,7 @@ GZIP_SKIP_PREFIXES: tuple[str, ...] = () SETTINGS_DEFAULTS = { "site_title": "Molodetz", "site_tagline": "De kring, niet de reclame.", + "site_intro": SITE_INTRO_SEED, "maintenance_mode": "0", "maintenance_message": "Molodetz is even dicht voor onderhoud.", "rate_limit_per_minute": "60", diff --git a/molodetz/database/init_db.py b/molodetz/database/init_db.py index 79acef6..8ea870f 100644 --- a/molodetz/database/init_db.py +++ b/molodetz/database/init_db.py @@ -157,9 +157,8 @@ def _seed_settings(): if "site_settings" not in db.tables: return for key, value in SETTINGS_DEFAULTS.items(): - uid = f"default_{key}" - if db["site_settings"].find_one(uid=uid) is None: - db["site_settings"].insert({"uid": uid, "key": key, "value": value, "updated_at": now_iso()}) + if db["site_settings"].find_one(key=key) is None: + db["site_settings"].insert({"uid": f"default_{key}", "key": key, "value": value, "updated_at": now_iso()}) def _seed_notification_key(): diff --git a/molodetz/database/settings.py b/molodetz/database/settings.py index 0b84051..5bbd01c 100644 --- a/molodetz/database/settings.py +++ b/molodetz/database/settings.py @@ -61,9 +61,7 @@ def get_setting(key, default=None): return cached value = default if "site_settings" in db.tables: - row = db["site_settings"].find_one(uid=f"setting_{key}") - if row is None: - row = db["site_settings"].find_one(uid=f"default_{key}") + row = db["site_settings"].find_one(key=key) if row is not None and row.get("value") is not None: value = str(row["value"]) _settings_cache.set(key, value) @@ -78,10 +76,13 @@ def get_int_setting(key, default): def set_setting(key, value): - db["site_settings"].upsert( - {"uid": f"setting_{key}", "key": key, "value": str(value), "updated_at": now_iso()}, - ["uid"], - ) + table = db["site_settings"] + stamp = now_iso() + with db: + if table.find_one(key=key) is None: + table.insert({"uid": f"setting_{key}", "key": key, "value": str(value), "updated_at": stamp}) + else: + table.update({"key": key, "value": str(value), "updated_at": stamp}, ["key"]) bump_cache_version("settings", _settings_cache) diff --git a/molodetz/models.py b/molodetz/models.py index 5ba4693..1afece9 100644 --- a/molodetz/models.py +++ b/molodetz/models.py @@ -104,6 +104,7 @@ class RoleForm(FormModel): class SettingsForm(FormModel): site_title: str | None = Field(default=None, max_length=80) site_tagline: str | None = Field(default=None, max_length=200) + site_intro: str | None = Field(default=None, max_length=20000) maintenance_mode: str | None = None maintenance_message: str | None = Field(default=None, max_length=500) rate_limit_per_minute: str | None = None @@ -115,6 +116,11 @@ class SettingsForm(FormModel): audit_retention_days: str | None = None outbound_proxy_url: str | None = Field(default=None, max_length=500) + @field_validator("site_intro") + @classmethod + def normalize_newlines(cls, value): + return value.replace("\r\n", "\n").replace("\r", "\n") if value else value + class ServiceConfigForm(FormModel): model_config = ConfigDict(extra="allow", str_strip_whitespace=True) diff --git a/molodetz/routers/home.py b/molodetz/routers/home.py index 7cec5f9..a4db611 100644 --- a/molodetz/routers/home.py +++ b/molodetz/routers/home.py @@ -3,8 +3,10 @@ from fastapi import APIRouter, Request from molodetz import config from molodetz.cache import TTLCache -from molodetz.database import count_media, published_posts +from molodetz.constants import SITE_INTRO_SEED +from molodetz.database import count_media, get_setting, published_posts from molodetz.gallery import gallery_items +from molodetz.rendering import render_content from molodetz.responses import respond from molodetz.schemas.posts import HomeOut from molodetz.seo import base_seo_context, organization_schema, website_schema @@ -39,4 +41,6 @@ async def index(request: Request): description_for("home"), schemas=[website_schema(request), organization_schema(request)], ) - return respond(request, "home.html", {**blocks, "seo": seo}, model=HomeOut) + intro = get_setting("site_intro", SITE_INTRO_SEED) + ctx = {**blocks, "intro": intro, "intro_html": render_content(intro), "seo": seo} + return respond(request, "home.html", ctx, model=HomeOut) diff --git a/molodetz/schemas/posts.py b/molodetz/schemas/posts.py index a6add57..311c378 100644 --- a/molodetz/schemas/posts.py +++ b/molodetz/schemas/posts.py @@ -28,6 +28,7 @@ class PostDetailOut(OutModel): class HomeOut(OutModel): + intro: str = "" posts: list[PostOut] = [] flyer_count: int = 0 meme_count: int = 0 diff --git a/molodetz/seed/intro.md b/molodetz/seed/intro.md new file mode 100644 index 0000000..aa9d84d --- /dev/null +++ b/molodetz/seed/intro.md @@ -0,0 +1,26 @@ + +I wrote more code than you. I wrote more code than the best programmer you know. I wrote more code than you two together. + +Yes, that pill is hard to swallow. I do understand that. I am running a keylogger that i wrote myself in C many, many years ago. +I ended up with a sqlite3 database with more than 60.000.000 rows in it and it runs better than your production server and it will stay that way. +When I created this process, I did not know why, I thought, it must have been worth SOMETHING. I just do what the big companies do, collect data and find out later, what to do with it. +Well, for starters, I can extract with AI what I did every hour. Yes, I know what I did for many years literally every hour with a detailed description. A zero effort hour registration system. +And did you know what that system told me? I wrote more code than you. I wrote more code than the best programmer you know. I wrote more code than you two together. To top it off, a plugin for ai autocompletion did measure its usage among millions of programmers (Codeium). I was in the top 0.01. Sad that it didn't go lower, since I came late to the party and was only using it for a half year. +Once, I published automated my working hours on my website automatically. Why not. Doing good stuff is something worth publishing right. Well, I took it down because people considered 72hrs in three days a bit disturbing, especially since it was not an anomaly. + +And yes, I am very proud of that. I do not watch Netflix, I do not play games, I rarely do watch something not code related on youtube. I only work on developing myself and that is the target product. I do not develop software with goal to deliver great software. I develop software for the sake of development. Because, it makes as high as fuck. Developing new skills are the best feeling there is. I really feel amazing and according the book "The subtle art of not giving a fuck" explains literally why. Great book. Buy now. It really teaches you how to become happy. A method that can't fail. + +Do not let people tell you, that you are flexing why you are just stating the truth. I know, it hurts if you do not have anything to flex yourself. + +So, my previous site had only the 0.01% claim backed by Codeium and, you have literally, no idea, how absurdly people reacted to it. Completely crazy. It was just a random statement from the biggest ai autocompletion plugin. It just tracked that. That's all. +It even costed me a friend. I don't understand, i even linked to my profile page on codeium and ... +Also, when some stuff i host is malfunctioning, it can lead up to multiple forum posts with some referring to that statistic. + +I thought, maybe I just delete it. But i can't delete it. It is all i have in life and you know what? It was even a freaking understatement and i triple down backed by data. + +You have feelings, i have data. One of them is reliable. + +Anyway, welcome to my new hostile website. No professional texts anymore. Just me, taking down all people that refuse to use AI. Also I take down people that refuse to learn to code (Vibe "Coders"). And, to promote a product I am working on (but i am working with) for weeks. I am working on a development environment for the elite. Fun fact, you already know the environment if you're the classic elite (vim, tmux, etc with the pretty well know adjustments). But what people extremely underestimate is that AI is something that needs to be learnt as well. AI didn't make us equal at all. We are more divided in skills than ever before. +And let's see, if dividing people is as fun as it looks and if i can offer a solution for the problem. + +Will you be the one that will be replaced? Or will you be the one that will replace. Look at the guy next to you. diff --git a/molodetz/static/css/content.css b/molodetz/static/css/content.css index f4cd332..2a0e815 100644 --- a/molodetz/static/css/content.css +++ b/molodetz/static/css/content.css @@ -12,6 +12,13 @@ .hero .lead { font-size: var(--text-lg); color: var(--ink-soft); max-width: 30em; margin: 0; } .hero .lead em { color: var(--ink); font-style: italic; } +.intro { max-width: var(--measure); font-family: var(--font-serif); color: var(--ink-soft); font-size: var(--text-base); line-height: var(--leading-body); } +.intro p { margin: 0 0 var(--space-md); } +.intro > p:first-child { font-size: var(--text-xl); line-height: 1.3; color: var(--ink); letter-spacing: -0.01em; margin-bottom: var(--space-lg); } +.intro > p:last-child { margin-bottom: 0; } +.intro-edit { font-family: var(--font-sans); font-size: var(--text-xs); margin: var(--space-md) 0 0; } +.intro-edit a { color: var(--muted); } + .section-head { display: flex; justify-content: space-between; @@ -125,4 +132,5 @@ @media (max-width: 480px) { .article-header h1 { font-size: var(--text-xl); } .prose { font-size: var(--text-base); } + .intro > p:first-child { font-size: var(--text-lg); } } diff --git a/molodetz/templates/admin/settings.html b/molodetz/templates/admin/settings.html index 9e2ce4c..2f5b6ff 100644 --- a/molodetz/templates/admin/settings.html +++ b/molodetz/templates/admin/settings.html @@ -6,7 +6,10 @@ {% for key, value in settings.items() %}
- {% if key in ('maintenance_mode', 'friendly_404') %} + {% if key == 'site_intro' %} + + Intro bovenaan de homepage. Markdown; elke regelovergang blijft staan, een lege regel begint een nieuwe alinea. De eerste alinea wordt groot gezet. + {% elif key in ('maintenance_mode', 'friendly_404') %} {% else %} diff --git a/molodetz/templates/docs/prose/beheer.md b/molodetz/templates/docs/prose/beheer.md index 92d86b3..185a895 100644 --- a/molodetz/templates/docs/prose/beheer.md +++ b/molodetz/templates/docs/prose/beheer.md @@ -10,4 +10,6 @@ Onder *Beheer, Services* staan alle achtergrondservices. Elke service heeft een ## Onderhoudsmodus +De intro bovenaan de homepage staat in de instelling `site_intro` (markdown, regelovergangen blijven staan). Bewerk hem onder *Instellingen*; de eerste alinea wordt groot gezet. + Zet `maintenance_mode` op `1` onder *Instellingen*. Beheerders blijven alles zien. diff --git a/molodetz/templates/home.html b/molodetz/templates/home.html index 0f01f84..0ddf878 100644 --- a/molodetz/templates/home.html +++ b/molodetz/templates/home.html @@ -1,10 +1,12 @@ {% extends "base.html" %} {% block content %} +{% set viewer = current_user(request) %}
Een blogrol

Molodetz

-

Een kleine kring die schrijft over het werk. De standaard ligt boven wat nu gewoon lijkt. Geen cursus. Geen geschreeuw. Wel een mening.

+
{{ intro_html | safe }}
+ {% if viewer and is_admin(viewer) %}

Intro bewerken

{% endif %}
diff --git a/pyproject.toml b/pyproject.toml index 5dda73a..f6b0145 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "molodetz" -version = "1.0.5" +version = "1.0.6" description = "Molodetz, a calm community blog roll." readme = "README.md" requires-python = ">=3.12" @@ -53,7 +53,7 @@ molodetz = "molodetz.cli.main:main" include = ["molodetz*"] [tool.setuptools.package-data] -molodetz = ["templates/**/*", "static/**/*", "docs_api/*.py"] +molodetz = ["templates/**/*", "static/**/*", "docs_api/*.py", "seed/*.md"] [tool.pytest.ini_options] testpaths = ["tests/unit", "tests/api", "tests/e2e"] diff --git a/tests/api/index.py b/tests/api/index.py index c9280ad..802c0b7 100644 --- a/tests/api/index.py +++ b/tests/api/index.py @@ -13,3 +13,49 @@ def test_unknown_page_is_404(anon): response = anon.get("/bestaat-niet-xyz") assert response.status_code == 404 assert anon.get("/bestaat-niet-xyz", headers={"Accept": "application/json"}).json()["error"] + + +def _intro_section(html): + from bs4 import BeautifulSoup + + return BeautifulSoup(html, "html.parser").select_one("section.hero [data-intro]") + + +def test_intro_renders_on_home(anon): + from molodetz.constants import SITE_INTRO_SEED + + intro = _intro_section(anon.get("/").text) + assert intro is not None + paragraphs = intro.find_all("p", recursive=False) + assert len(paragraphs) == len([block for block in SITE_INTRO_SEED.split("\n\n") if block.strip()]) + assert paragraphs[0].get_text(" ", strip=True).startswith("I wrote more code than you. I wrote more code than the best programmer you know.") + assert paragraphs[-1].get_text(strip=True) == "Will you be the one that will be replaced? Or will you be the one that will replace. Look at the guy next to you." + assert len(paragraphs[1].find_all("br")) == 5 + text = intro.get_text(" ", strip=True) + for fixed in ("especially since it was", "Well, for starters", "development environment", "more divided in skills", "i triple down", "No professional texts", "it must have been worth", "measure its usage", "Buy now.", 'Vibe "Coders"'): + assert fixed in text, fixed + for typo in ("sicne", "deelopment", "devided", "tripple", "teksts", "have be worth", "it s usage", "\u1e82"): + assert typo not in text, typo + assert "