New English meme and flyer set, retire Dutch-text gallery images

This commit is contained in:
2026-10-05 11:42:35 +02:00
parent ff547e1677
commit 1e16334a0e
30 changed files with 121 additions and 26 deletions
+9
View File
@@ -37,6 +37,15 @@ def upsert_media(filename, kind, caption, position, width, height, phash, thumb_
return fields["uid"]
def retire_media(filename):
existing = get_media_by_file(filename)
if not existing or existing.get("deleted_at"):
return False
stamp = now_iso()
db["media_items"].update({"id": existing["id"], "deleted_at": stamp, "deleted_by": "system", "updated_at": stamp}, ["id"])
return True
def count_media(kind=None):
if "media_items" not in db.tables:
return 0
+1 -1
View File
@@ -6,7 +6,7 @@ GROUP = {
"title": "Content",
"description": "Public read routes: the roll, the standard, posts, galleries and people.",
"endpoints": [
endpoint("GET", "/", "Home", "Latest posts and the featured flyer.", sample={"posts": [], "flyer_count": 4, "meme_count": 20}),
endpoint("GET", "/", "Home", "Latest posts and the featured flyer.", sample={"posts": [], "flyer_count": 4, "meme_count": 22}),
endpoint(
"GET",
"/roll",
+51 -21
View File
@@ -8,7 +8,7 @@ from PIL import Image
from molodetz import config
from molodetz.attachments import blob_path, blob_url, write_blob
from molodetz.database import generate_uid, media_by_kind, upsert_media
from molodetz.database.media import get_media_by_file
from molodetz.database.media import get_media_by_file, retire_media
logger = logging.getLogger(__name__)
@@ -16,32 +16,57 @@ THUMB_WIDTH = 720
DUPLICATE_DISTANCE = 2
GALLERY = (
("flyer-keuzes.jpg", "flyer", "Thirty-one choices. You have zero."),
("flyer-chat.jpg", "flyer", "A chat is not a portfolio."),
("flyer-bijna.jpg", "flyer", "Almost is not a state."),
("flyer-assistent.jpg", "flyer", "The assistant does not judge."),
("meme-vloer.jpg", "meme", "A pass is fine. The floor, you mean."),
("meme-split.jpg", "meme", "Send them home."),
("meme-cursus.jpg", "meme", "No ebook. Just finish it."),
("take-chatlog.jpg", "meme", "Your portfolio is a chat log."),
("take-copilot.jpg", "meme", "Copilot is not a senior."),
("take-coping.jpg", "meme", "A pass is coping."),
("poster-thuis.jpg", "meme", "Vibe coders, go home."),
("poster-vloer.jpg", "meme", "A pass? That is the floor."),
("poster-cursus.jpg", "meme", "No course. Just finish it."),
("poster-luiheid.jpg", "meme", "AI is not taking your job. Laziness is."),
("poster-spel.jpg", "meme", "The game has turned. You have not."),
("story.jpg", "meme", "No ebook. No 10x developer. Just finishing."),
("meme-zes.jpg", "meme", "A pass. Congratulations, you are average."),
("meme-gesprek.jpg", "meme", "My portfolio is a conversation."),
("meme-product.jpg", "meme", "The course is the product. The work is not."),
("meme-bijna.jpg", "meme", "Everyone lines up at almost."),
("flyer-elite-twice.jpg", "flyer", "Elite twice. Know the code. Use the machine. Read every line it writes."),
("flyer-taste.jpg", "flyer", "Taste does not autocomplete."),
("flyer-demo.jpg", "flyer", "A demo is not a product."),
("flyer-hands.jpg", "flyer", "Your hands are not the craft."),
("meme-elite-galaxy-brain.jpg", "meme", "Getting elite twice: know every line before the AI writes it, review every line after, ship by lunch."),
("meme-classic-autocomplete.jpg", "meme", "I don't need autocomplete. Hand-typed. Artisanal. Not shipped."),
("meme-vibe-prompt-and-pray.jpg", "meme", "Prompt and pray: the same TypeError pasted into the chat 31 times. Suggested fix: learn what a function is."),
("meme-elite-choose-your-fighter.jpg", "meme", "Choose your fighter: the classic, the vibe coder, or the elite who writes code, uses AI and reads every diff."),
("meme-classic-starter-pack.jpg", "meme", "The 'AI is just fancy autocomplete' starter pack: 36 tmux panes, a vimrc older than the intern, zero pull requests."),
("meme-vibe-starter-pack.jpg", "meme", "The 'built it in a weekend' starter pack: tab, tab, accept all, API key committed to main, works on my machine."),
("meme-elite-build-log.jpg", "meme", "Elite twice. The model types. You decide. 1,184 lines drafted, every line read, 212 rejected."),
("meme-classic-chart.jpg", "meme", "Free-range, hand-typed code: keystrokes up, features shipped flat, opinions about AI off the chart."),
("meme-vibe-galaxy-brain.jpg", "meme", "Vibe debugging: read the error, paste the error, tab tab accept all, delete the repo and prompt a new one."),
("meme-elite-take.jpg", "meme", "AI replaced the part of me that typed. The part that thinks got a raise."),
("meme-classic-same-guy.jpg", "meme", "1987: compilers make programmers lazy. 2026: AI makes programmers lazy. Same guy. Still not shipping."),
("meme-vibe-git-blame.jpg", "meme", "Git blame says it was you. You say it was the vibes."),
("meme-elite-curve.jpg", "meme", "Pick your curve: classic stays flat, the vibe coder crashes at the first prod incident, elite plus AI keeps climbing."),
("meme-classic-certificate.jpg", "meme", "Certificate of manual labour. Time saved: 0 hours. Moral high ground: maximum."),
("meme-vibe-weekend.jpg", "meme", "A vibe coder's weekend: a SaaS on Friday, a public database by Sunday, a course about it on Monday."),
("meme-elite-achievement.jpg", "meme", "Achievement unlocked: this developer embraced AI and is now elite twice."),
("meme-classic-family.jpg", "meme", "Three generations of refusing the tool. Three generations of getting outshipped."),
("meme-vibe-priorities.jpg", "meme", "Vibe coder priorities: learning what a for loop does, nah. Typing 'make it work, no bugs, please' 30 times, yep."),
("meme-compiles.jpg", "meme", "It compiles. Ship it, king."),
("meme-senior.jpg", "meme", "My copilot is the senior."),
("meme-trophy.jpg", "meme", "Peak performance, 6 out of 10."),
("meme-todo.jpg", "meme", "Side projects, a love story."),
)
RETIRED_GALLERY = (
"flyer-keuzes.jpg",
"flyer-chat.jpg",
"flyer-bijna.jpg",
"flyer-assistent.jpg",
"meme-vloer.jpg",
"meme-split.jpg",
"meme-cursus.jpg",
"meme-zes.jpg",
"meme-gesprek.jpg",
"meme-product.jpg",
"meme-bijna.jpg",
"poster-thuis.jpg",
"poster-vloer.jpg",
"poster-cursus.jpg",
"poster-luiheid.jpg",
"poster-spel.jpg",
"story.jpg",
"take-chatlog.jpg",
"take-copilot.jpg",
"take-coping.jpg",
)
def _thumbnail_bytes(image):
copy = image.convert("RGB")
@@ -51,7 +76,12 @@ def _thumbnail_bytes(image):
return buffer.getvalue()
def retire_gallery():
return sum(1 for filename in RETIRED_GALLERY if retire_media(filename))
def sync_gallery():
retire_gallery()
seen_hashes = []
synced = 0
for position, (filename, kind, caption) in enumerate(GALLERY):
+1 -1
View File
@@ -6,7 +6,7 @@ from molodetz import config
from molodetz.assets import static_url
from molodetz.seo_meta_text import DEFAULT_DESCRIPTION
DEFAULT_OG_IMAGE_PATH = "/static/media/flyer-keuzes.jpg"
DEFAULT_OG_IMAGE_PATH = "/static/media/flyer-elite-twice.jpg"
def base_url(request):
Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

+1 -1
View File
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "molodetz"
version = "1.0.7"
version = "1.0.8"
description = "Molodetz, a calm community blog roll."
readme = "README.md"
requires-python = ">=3.12"
+19
View File
@@ -5,3 +5,22 @@ def test_galleries_have_images(anon):
for path, minimum in (("/flyers", 4), ("/memes", 10)):
html = anon.get(path).text
assert html.count("data-lightbox") >= minimum, path
def test_retired_dutch_images_are_not_listed(anon):
from molodetz.gallery import RETIRED_GALLERY
for path in ("/flyers", "/memes"):
items = anon.get(path, headers={"Accept": "application/json"}).json()["items"]
names = {item["filename"] for item in items}
assert not names & set(RETIRED_GALLERY), path
assert all(item["caption"] for item in items), path
def test_home_features_the_elite_twice_flyer(anon):
html = anon.get("/").text
assert "flyer-elite-twice.jpg" in html
assert "Elite twice." in html
counts = anon.get("/", headers={"Accept": "application/json"}).json()
assert counts["flyer_count"] == 4
assert counts["meme_count"] >= 18
+1 -1
View File
@@ -43,7 +43,7 @@ def test_sitemap_lists_english_paths(anon):
def test_gallery_captions_are_english(anon):
flyers = anon.get("/flyers", headers={"Accept": "application/json"}).json()["items"]
captions = {item["caption"] for item in flyers}
assert "Thirty-one choices. You have zero." in captions
assert "Taste does not autocomplete." in captions
assert not any(" geen " in f" {caption.lower()} " or " een " in f" {caption.lower()} " for caption in captions)
+38 -1
View File
@@ -1,6 +1,8 @@
# retoor <retoor@molodetz.nl>
from molodetz import config
from molodetz.gallery import GALLERY
from PIL import Image
from molodetz.gallery import GALLERY, RETIRED_GALLERY
BANNED = ("selfie", "mirror", "devplace", "nigel", "nldr", "waifu", "zwem")
@@ -16,3 +18,38 @@ def test_gallery_files_exist_and_are_allowed():
def test_both_kinds_present():
kinds = {entry["kind"] if isinstance(entry, dict) else entry[1] for entry in GALLERY}
assert {"flyer", "meme"} <= kinds
def test_gallery_image_sizes_match_kind():
for name, kind, _ in GALLERY:
if not name.startswith(("flyer-elite", "flyer-taste", "flyer-demo", "flyer-hands", "meme-classic", "meme-vibe", "meme-elite")):
continue
with Image.open(config.MEDIA_SOURCE_DIR / name) as image:
assert image.size == ((1080, 1350) if kind == "flyer" else (1080, 1080)), name
def test_retired_files_stay_on_disk_but_leave_the_gallery():
names = {entry[0] for entry in GALLERY}
assert len(RETIRED_GALLERY) == 20
for name in RETIRED_GALLERY:
assert name not in names, name
assert (config.MEDIA_SOURCE_DIR / name).is_file(), name
def test_gallery_captions_are_clean_english():
for name, _, caption in GALLERY:
assert caption and "\u2014" not in caption, name
assert not any(word in caption.lower() for word in BANNED), name
assert GALLERY[0][0] == "flyer-elite-twice.jpg"
def test_retire_media_soft_deletes_once(app_server):
from molodetz.database import db
from molodetz.database.media import media_by_kind, retire_media, upsert_media
upsert_media("story.jpg", "meme", "Old Dutch story", 999, 1080, 1350, None, None, None)
assert retire_media("story.jpg") is True
assert retire_media("story.jpg") is False
row = db["media_items"].find_one(filename="story.jpg")
assert row["deleted_at"] and row["deleted_by"] == "system"
assert "story.jpg" not in {item["filename"] for item in media_by_kind("meme")}