Organize gallery into highlighted photo groups

This commit is contained in:
2026-09-19 18:13:48 +03:30
parent 3ec6cdd3ec
commit b6b19c07bb
30 changed files with 335 additions and 66 deletions
+1 -1
View File
@@ -3,8 +3,8 @@ layout: post
title: x_DSC-0999.jpg
date: 2024-10-09
categories: art
anchor: "Kazakhstan - World Nomad Games"
---
![x](/img/arts/kazakhstan-2024/x_DSC-0999.jpg)
@@ -3,6 +3,7 @@ layout: post
title: Taq-e-Bostan_000521270020.jpg
date: 2024-10-25
categories: art
anchor: "Iran - Hamadan, Kermanshah, Tabriz"
---
![Taq e Bostan](/img/arts/iran-analog-2024/Taq-e-Bostan_000521270020.jpg)
@@ -1,7 +1,7 @@
---
layout: post
title: "photo-260cc99527c13f256d4ea39d0959f5daa942c888ea882852e73fd94f6c30b7db"
date: "2026-09-19T12:10:40.964254+00:00"
date: "2026-09-19T12:29:32.999986+00:00"
categories: art
---
@@ -1,7 +1,7 @@
---
layout: post
title: "photo-474ab3283ce8801ac6e59b67b65b4169167053873a5a8db4a15b840a3448266c"
date: "2026-09-19T12:29:33.758101+00:00"
date: "2026-09-19T12:29:32.999994+00:00"
categories: art
---
@@ -0,0 +1,9 @@
---
layout: post
title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-00"
date: "2026-09-19T12:29:32.999993+00:00"
categories: art
anchor: "Romania"
---
<p><img src="/img/arts/uploads/7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d/00.jpg" alt="" loading="lazy"></p>
@@ -0,0 +1,8 @@
---
layout: post
title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-01"
date: "2026-09-19T12:29:32.999992+00:00"
categories: art
---
<p><img src="/img/arts/uploads/7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d/01.jpg" alt="" loading="lazy"></p>
@@ -0,0 +1,8 @@
---
layout: post
title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-02"
date: "2026-09-19T12:29:32.999984+00:00"
categories: art
---
<p><img src="/img/arts/uploads/7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d/02.jpg" alt="" loading="lazy"></p>
@@ -0,0 +1,8 @@
---
layout: post
title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-03"
date: "2026-09-19T12:29:32.999983+00:00"
categories: art
---
<p><img src="/img/arts/uploads/7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d/03.jpg" alt="" loading="lazy"></p>
@@ -0,0 +1,8 @@
---
layout: post
title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-04"
date: "2026-09-19T12:29:32.999995+00:00"
categories: art
---
<p><img src="/img/arts/uploads/7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d/04.jpg" alt="" loading="lazy"></p>
@@ -0,0 +1,8 @@
---
layout: post
title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-05"
date: "2026-09-19T12:29:32.999996+00:00"
categories: art
---
<p><img src="/img/arts/uploads/7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d/05.jpg" alt="" loading="lazy"></p>
@@ -0,0 +1,8 @@
---
layout: post
title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-06"
date: "2026-09-19T12:29:32.999997+00:00"
categories: art
---
<p><img src="/img/arts/uploads/7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d/06.jpg" alt="" loading="lazy"></p>
@@ -0,0 +1,9 @@
---
layout: post
title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-07"
date: "2026-09-19T12:29:32.999998+00:00"
categories: art
---
<p><img src="/img/arts/uploads/7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d/07.jpg" alt="Transmission of Flesh by Erfan Ashourioun" loading="lazy"></p>
<span class="image-details">Transmission of Flesh by Erfan Ashourioun</span>
@@ -0,0 +1,8 @@
---
layout: post
title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-08"
date: "2026-09-19T12:29:32.999999+00:00"
categories: art
---
<p><img src="/img/arts/uploads/7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d/08.jpg" alt="" loading="lazy"></p>
@@ -0,0 +1,9 @@
---
layout: post
title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d"
date: "2026-09-19T12:29:33+00:00"
categories: art
anchor: "Armenia"
---
<p><img src="/img/arts/uploads/7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d/09.jpg" alt="" loading="lazy"></p>
@@ -1,19 +0,0 @@
---
layout: post
title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d"
date: "2026-09-19T12:27:27.038898+00:00"
categories: art
---
<div id="photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d"></div>
<p><img src="/img/arts/uploads/7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d/00.jpg" alt="" loading="lazy"></p>
<p><img src="/img/arts/uploads/7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d/01.jpg" alt="" loading="lazy"></p>
<p><img src="/img/arts/uploads/7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d/02.jpg" alt="" loading="lazy"></p>
<p><img src="/img/arts/uploads/7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d/03.jpg" alt="" loading="lazy"></p>
<p><img src="/img/arts/uploads/7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d/04.jpg" alt="" loading="lazy"></p>
<p><img src="/img/arts/uploads/7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d/05.jpg" alt="" loading="lazy"></p>
<p><img src="/img/arts/uploads/7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d/06.jpg" alt="" loading="lazy"></p>
<p><img src="/img/arts/uploads/7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d/07.jpg" alt="" loading="lazy"></p>
<p><img src="/img/arts/uploads/7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d/08.jpg" alt="" loading="lazy"></p>
<p><img src="/img/arts/uploads/7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d/09.jpg" alt="" loading="lazy"></p>
@@ -1,7 +1,7 @@
---
layout: post
title: "photo-b18a2f1c7127077c801160782012cb164759f0b9dd41762205b45e4c9a07d673"
date: "2026-09-19T12:03:11.135424+00:00"
date: "2026-09-19T12:29:32.999985+00:00"
categories: art
---
@@ -0,0 +1,10 @@
---
layout: post
title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b"
date: "2026-09-19T12:29:32.999991+00:00"
categories: art
---
<div id="photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b"></div>
<p><img src="/img/arts/uploads/c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b/00.jpg" alt="" loading="lazy"></p>
@@ -0,0 +1,8 @@
---
layout: post
title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-01"
date: "2026-09-19T12:29:32.999990+00:00"
categories: art
---
<p><img src="/img/arts/uploads/c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b/01.jpg" alt="" loading="lazy"></p>
@@ -0,0 +1,8 @@
---
layout: post
title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-02"
date: "2026-09-19T12:29:32.999989+00:00"
categories: art
---
<p><img src="/img/arts/uploads/c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b/02.jpg" alt="" loading="lazy"></p>
@@ -0,0 +1,8 @@
---
layout: post
title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-03"
date: "2026-09-19T12:29:32.999988+00:00"
categories: art
---
<p><img src="/img/arts/uploads/c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b/03.jpg" alt="" loading="lazy"></p>
@@ -0,0 +1,8 @@
---
layout: post
title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-04"
date: "2026-09-19T12:29:32.999987+00:00"
categories: art
---
<p><img src="/img/arts/uploads/c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b/04.jpg" alt="" loading="lazy"></p>
@@ -1,14 +0,0 @@
---
layout: post
title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b"
date: "2026-09-19T12:26:31.454056+00:00"
categories: art
---
<div id="photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b"></div>
<p><img src="/img/arts/uploads/c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b/00.jpg" alt="" loading="lazy"></p>
<p><img src="/img/arts/uploads/c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b/01.jpg" alt="" loading="lazy"></p>
<p><img src="/img/arts/uploads/c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b/02.jpg" alt="" loading="lazy"></p>
<p><img src="/img/arts/uploads/c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b/03.jpg" alt="" loading="lazy"></p>
<p><img src="/img/arts/uploads/c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b/04.jpg" alt="" loading="lazy"></p>
+7 -4
View File
@@ -150,10 +150,13 @@ Create **Publish to blog**:
only `published` means the deployed site's receipt contains this album.
The ZIP route accepts 1–20 images in archive-entry order and keeps them as one
upload. Each photo renders as its own gallery `<li>`, including photos in older
batches. The caption appears on each photo; the first image is the highlight target. Metadata sidecars
under `__MACOSX/` and `.DS_Store` are ignored. Ordinary API clients can also send
repeated `photos` parts; send either `archive` or `photos`, never both.
upload job. Each photo is stored as its own `_art` document and renders as its
own gallery `<li>`. The shared caption is repeated on each photo and an optional
highlight begins on the first photo. Photos remain contiguous in the selection order.
Older batch documents are still split at render time for backward compatibility.
Metadata sidecars under `__MACOSX/` and `.DS_Store` are ignored. Ordinary API
clients can also send repeated `photos` parts; send either `archive` or `photos`,
never both.
Select still photos; videos/Live Photo video components aren't supported. Dates
are upload time in UTC; captions are plain text. A caption is optional.
An optional highlight name (up to 200 characters) adds one link to **Highlights**,
+30 -16
View File
@@ -10,7 +10,7 @@ from pathlib import Path
import re
import shutil
import tempfile
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
import warnings
import zipfile
@@ -79,6 +79,26 @@ def web_image(raw, destination):
Image.Resampling.LANCZOS)
def photo_entry(job, date, index, count, caption, highlight):
# Give each photo its own collection document. Descending dates preserve the
# selection order because the gallery renders the newest document first.
item_date = datetime.fromisoformat(date) + timedelta(microseconds=count - index)
title = f"photo-{job}" if index == 0 else f"photo-{job}-{index:02}"
content = ["---", "layout: post", f'title: "{title}"',
f'date: "{item_date.isoformat()}"', "categories: art"]
if index == 0 and highlight:
content.append(f"anchor: {json.dumps(highlight)}")
content.extend(["---", ""])
if index == 0 and not highlight:
content.extend([f'<div id="photo-{job}"></div>', ""])
content.append(f'<p><img src="/img/arts/uploads/{job}/{index:02}.jpg" '
f'alt="{public_text(caption)}" loading="lazy"></p>')
if caption:
content.append('<span class="image-details">' +
public_text(caption).replace("\n", "<br>") + "</span>")
return "\n".join(content) + "\n"
def create_app(data_dir=None, token=None):
app = Flask(__name__)
app.config.update(MAX_CONTENT_LENGTH=100 * 1024 * 1024,
@@ -261,21 +281,15 @@ def create_app(data_dir=None, token=None):
if kind == "snippet":
manifest.update(kind=kind, text=text)
atomic_json(temporary / "manifest.json", manifest)
content = ["---", "layout: post", f'title: "photo-{job}"',
f'date: "{date}"', "categories: art"]
if highlight:
content.append(f"anchor: {json.dumps(highlight)}")
content.extend(["---", ""])
# The gallery already renders an anchor before the first photo for highlights.
if not highlight:
content.extend([f'<div id="photo-{job}"></div>', ""])
for index in range(len(photos)):
content.append(f'<p><img src="/img/arts/uploads/{job}/{index:02}.jpg" '
f'alt="{public_text(caption)}" loading="lazy"></p>')
if caption:
content.append('<span class="image-details">' +
public_text(caption).replace("\n", "<br>") + "</span>")
if kind == "snippet":
if kind == "photo":
(temporary / "entries").mkdir()
for index in range(len(photos)):
(temporary / "entries" / f"{index:02}.md").write_text(
photo_entry(job, date, index, len(photos), caption, highlight))
# Retain the old filename for compatibility with publisher versions
# that predate one-document-per-photo submissions.
content = [(temporary / "entries/00.md").read_text()]
else:
content = [f'<div id="snippet-{job}"></div>', ""]
for paragraph in (text, caption):
if paragraph:
+13 -2
View File
@@ -86,9 +86,20 @@ class Publisher:
entry = Path("_art") / f"{manifest['date'][:10]}-photo-{job}.md"
images = Path("img/arts/uploads") / job
(self.repo / entry).parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(submission / "entry.md", self.repo / entry)
entries = submission / "entries"
if entries.is_dir():
# Remove the former batch document when a queued submission
# is republished by a newer service version.
self.git("rm", "--ignore-unmatch", "--", str(entry))
for source in sorted(entries.glob("*.md")):
destination = Path("_art") / f"{manifest['date'][:10]}-photo-{job}-{source.stem}.md"
shutil.copyfile(source, self.repo / destination)
self.git("add", "--", str(destination))
else:
shutil.copyfile(submission / "entry.md", self.repo / entry)
self.git("add", "--", str(entry))
shutil.copytree(submission / "images", self.repo / images, dirs_exist_ok=True)
self.git("add", "--", str(entry), str(images))
self.git("add", "--", str(images))
if self.git("diff", "--cached", "--name-only"):
self.git("commit", "-m", f"Publish {len(jobs)} submission(s)")
self.git("push", "origin", f"HEAD:refs/heads/{self.branch}")
+11 -6
View File
@@ -116,10 +116,12 @@ def test_album_caption_order_and_retry_deduplication(service):
assert response.status_code == 202
job = response.json["id"]
folder = data / "submissions" / job
entry = (folder / "entry.md").read_text()
assert "<script>" not in entry and "{%" not in entry and "{{" not in entry
assert "<br>second line" in entry
assert entry.index("00.jpg") < entry.index("01.jpg")
entries = sorted((folder / "entries").glob("*.md"))
assert len(entries) == 2
rendered = "\n".join(entry.read_text() for entry in entries)
assert "<script>" not in rendered and "{%" not in rendered and "{{" not in rendered
assert rendered.count("<br>second line") == 2
assert "00.jpg" in entries[0].read_text() and "01.jpg" in entries[1].read_text()
assert Image.open(folder / "images/00.jpg").getpixel((0, 0))[0] > 240
assert Image.open(folder / "images/01.jpg").getpixel((0, 0))[2] > 240
assert (folder / "originals/0").read_bytes() == images[0]
@@ -168,7 +170,9 @@ def test_highlight_album_and_safe_front_matter(service):
assert response.status_code == 202
job = response.json["id"]
folder = data / "submissions" / job
entry = (folder / "entry.md").read_text()
entries = sorted((folder / "entries").glob("*.md"))
assert len(entries) == 2
entry = entries[0].read_text()
metadata, body = entry.split("---\n", 2)[1:]
anchor = next(line for line in metadata.splitlines() if line.startswith("anchor: "))
assert json.loads(anchor.removeprefix("anchor: ")) == name
@@ -176,7 +180,8 @@ def test_highlight_album_and_safe_front_matter(service):
# The template emits the sole target at the start of this album, before 00.jpg.
assert f'title: "photo-{job}"' in metadata
assert f'id="photo-{job}"' not in body
assert body.index("00.jpg") < body.index("01.jpg")
assert "00.jpg" in body and "01.jpg" in entries[1].read_text()
assert "anchor:" not in entries[1].read_text()
assert json.loads((folder / "manifest.json").read_text())["highlight"] == name
assert upload(client, images, "Album caption", highlight=name).status_code == 200
assert upload(client, images, "Album caption", highlight="Other trip").json["id"] != job
+61
View File
@@ -0,0 +1,61 @@
"""One-time migration of September 2026 uploads into individual highlighted photos."""
from datetime import datetime, timedelta, timezone
from pathlib import Path
import html
ART = Path("_art")
UPLOAD = "/img/arts/uploads"
OLD = [
"260cc99527c13f256d4ea39d0959f5daa942c888ea882852e73fd94f6c30b7db",
"474ab3283ce8801ac6e59b67b65b4169167053873a5a8db4a15b840a3448266c",
"7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d",
"b18a2f1c7127077c801160782012cb164759f0b9dd41762205b45e4c9a07d673",
"c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b",
]
armenia = [
(OLD[2], 9), (OLD[2], 8), (OLD[2], 7), (OLD[2], 6),
(OLD[2], 5), (OLD[2], 4), (OLD[1], 0),
]
romania = [
(OLD[2], 0), (OLD[2], 1), (OLD[4], 0), (OLD[4], 1),
(OLD[4], 2), (OLD[4], 3), (OLD[4], 4), (OLD[0], 0), (OLD[3], 0),
]
ungrouped = [(OLD[2], 2), (OLD[2], 3)]
ordered = armenia + romania + ungrouped
assert len(ordered) == len(set(ordered)) == 18
anchors = {armenia[0]: "Armenia", romania[0]: "Romania"}
captions = {(OLD[2], 7): "Transmission of Flesh by Erfan Ashourioun"}
first_for_job = {}
for photo in ordered:
first_for_job.setdefault(photo[0], photo)
for job in OLD:
batch = next(ART.glob(f"*-photo-{job}.md"), None)
if batch:
batch.unlink()
start = datetime(2026, 9, 19, 12, 29, 33, tzinfo=timezone.utc)
for position, (job, index) in enumerate(ordered):
title = f"photo-{job}-{index:02}"
if (job, index) == first_for_job[job]:
title = f"photo-{job}"
date = start - timedelta(microseconds=position)
lines = ["---", "layout: post", f'title: "{title}"',
f'date: "{date.isoformat()}"', "categories: art"]
if (job, index) in anchors:
lines.append(f'anchor: "{anchors[(job, index)]}"')
lines.extend(["---", ""])
if (job, index) == first_for_job[job] and (job, index) not in anchors:
lines.extend([f'<div id="photo-{job}"></div>', ""])
caption = captions.get((job, index), "")
lines.append(f'<p><img src="{UPLOAD}/{job}/{index:02}.jpg" '
f'alt="{html.escape(caption, quote=True)}" loading="lazy"></p>')
if caption:
lines.append(f'<span class="image-details">{html.escape(caption)}</span>')
target = ART / f"2026-09-19-photo-{job}-{index:02}.md"
target.write_text("\n".join(lines) + "\n")
print("Created 18 individual photo documents in Armenia, Romania, and ungrouped order")
+1 -1
View File
@@ -4,7 +4,7 @@ import os
from pathlib import Path
import re
photos = sorted(set(re.findall(r'id="photo-([a-f0-9]{64})"', Path('_site/art/index.html').read_text())))
photos = sorted(set(re.findall(r'id=["\']photo-([a-f0-9]{64})["\']', Path('_site/art/index.html').read_text())))
Path('_site/photo-publication.json').write_text(json.dumps({
'commit': os.environ['GITHUB_SHA'], 'photos': photos,
'snippets': sorted(set(re.findall(r'id="snippet-([a-f0-9]{64})"',
+45
View File
@@ -0,0 +1,45 @@
from datetime import datetime
from pathlib import Path
import re
import unittest
ROOT = Path(__file__).resolve().parents[2]
ART = ROOT / "_art"
JOB = "7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d"
ARMENIA = [f"/img/arts/uploads/{JOB}/{index:02}.jpg" for index in (9, 8, 7, 6, 5, 4)] + [
"/img/arts/uploads/474ab3283ce8801ac6e59b67b65b4169167053873a5a8db4a15b840a3448266c/00.jpg"]
ROMANIA = [f"/img/arts/uploads/{JOB}/{index:02}.jpg" for index in (0, 1)] + [
f"/img/arts/uploads/c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b/{index:02}.jpg"
for index in range(5)] + [
"/img/arts/uploads/260cc99527c13f256d4ea39d0959f5daa942c888ea882852e73fd94f6c30b7db/00.jpg",
"/img/arts/uploads/b18a2f1c7127077c801160782012cb164759f0b9dd41762205b45e4c9a07d673/00.jpg"]
class PhotoHighlightTests(unittest.TestCase):
def test_uploaded_photos_are_individual_and_grouped(self):
records = []
for path in ART.glob("*photo-*.md"):
content = path.read_text()
images = re.findall(r'<img src="([^"]+/uploads/[^"]+)"', content)
self.assertEqual(len(images), 1, path)
date = re.search(r'^date: "([^"]+)"', content, re.MULTILINE)
anchor = re.search(r'^anchor: "([^"]+)"', content, re.MULTILINE)
records.append((datetime.fromisoformat(date.group(1)), images[0],
anchor.group(1) if anchor else None, content))
records.sort(reverse=True)
grouped = [record for record in records if record[1] in ARMENIA + ROMANIA]
self.assertEqual([record[1] for record in grouped], ARMENIA + ROMANIA)
self.assertEqual(grouped[0][2], "Armenia")
self.assertEqual(grouped[7][2], "Romania")
self.assertIn("Transmission of Flesh by Erfan Ashourioun", grouped[2][3])
def test_existing_collection_highlight_starts(self):
iran = (ART / "2024-10-25-Taq-e-Bostan_000521270020.md").read_text()
kazakhstan = (ART / "2024-10-09-x_DSC-0999.md").read_text()
self.assertIn('anchor: "Iran - Hamadan, Kermanshah, Tabriz"', iran)
self.assertIn('anchor: "Kazakhstan - World Nomad Games"', kazakhstan)
if __name__ == "__main__":
unittest.main()
+37
View File
@@ -0,0 +1,37 @@
import os
import runpy
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
class PhotoManifestTest(unittest.TestCase):
def test_photo_receipts_accept_single_and_double_quoted_ids(self):
first = "a" * 64
second = "b" * 64
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "_site/art").mkdir(parents=True)
(root / "_site/snippets").mkdir(parents=True)
(root / "_site/art/index.html").write_text(
f'<span id="photo-{first}"></span><span id=\'photo-{second}\'></span>'
)
(root / "_site/snippets/index.html").write_text("")
with patch("os.environ", {"GITHUB_SHA": "test-commit"}):
old_cwd = Path.cwd()
try:
os.chdir(root)
script = Path(__file__).resolve().parents[1] / "photo-manifest.py"
runpy.run_path(str(script))
finally:
os.chdir(old_cwd)
manifest = (root / "_site/photo-publication.json").read_text()
self.assertIn(first, manifest)
self.assertIn(second, manifest)
if __name__ == "__main__":
unittest.main()