Organize gallery into highlighted photo groups
This commit is contained in:
@@ -3,8 +3,8 @@ layout: post
|
|||||||
title: x_DSC-0999.jpg
|
title: x_DSC-0999.jpg
|
||||||
date: 2024-10-09
|
date: 2024-10-09
|
||||||
categories: art
|
categories: art
|
||||||
|
anchor: "Kazakhstan - World Nomad Games"
|
||||||
---
|
---
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ layout: post
|
|||||||
title: Taq-e-Bostan_000521270020.jpg
|
title: Taq-e-Bostan_000521270020.jpg
|
||||||
date: 2024-10-25
|
date: 2024-10-25
|
||||||
categories: art
|
categories: art
|
||||||
|
anchor: "Iran - Hamadan, Kermanshah, Tabriz"
|
||||||
---
|
---
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
layout: post
|
layout: post
|
||||||
title: "photo-260cc99527c13f256d4ea39d0959f5daa942c888ea882852e73fd94f6c30b7db"
|
title: "photo-260cc99527c13f256d4ea39d0959f5daa942c888ea882852e73fd94f6c30b7db"
|
||||||
date: "2026-09-19T12:10:40.964254+00:00"
|
date: "2026-09-19T12:29:32.999986+00:00"
|
||||||
categories: art
|
categories: art
|
||||||
---
|
---
|
||||||
|
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
layout: post
|
layout: post
|
||||||
title: "photo-474ab3283ce8801ac6e59b67b65b4169167053873a5a8db4a15b840a3448266c"
|
title: "photo-474ab3283ce8801ac6e59b67b65b4169167053873a5a8db4a15b840a3448266c"
|
||||||
date: "2026-09-19T12:29:33.758101+00:00"
|
date: "2026-09-19T12:29:32.999994+00:00"
|
||||||
categories: art
|
categories: art
|
||||||
---
|
---
|
||||||
|
|
||||||
+9
@@ -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>
|
||||||
+8
@@ -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>
|
||||||
+8
@@ -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>
|
||||||
+8
@@ -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>
|
||||||
+8
@@ -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>
|
||||||
+8
@@ -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>
|
||||||
+8
@@ -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>
|
||||||
+9
@@ -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>
|
||||||
+8
@@ -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>
|
||||||
+9
@@ -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>
|
||||||
-19
@@ -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
-1
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
layout: post
|
layout: post
|
||||||
title: "photo-b18a2f1c7127077c801160782012cb164759f0b9dd41762205b45e4c9a07d673"
|
title: "photo-b18a2f1c7127077c801160782012cb164759f0b9dd41762205b45e4c9a07d673"
|
||||||
date: "2026-09-19T12:03:11.135424+00:00"
|
date: "2026-09-19T12:29:32.999985+00:00"
|
||||||
categories: art
|
categories: art
|
||||||
---
|
---
|
||||||
|
|
||||||
+10
@@ -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>
|
||||||
+8
@@ -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>
|
||||||
+8
@@ -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>
|
||||||
+8
@@ -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>
|
||||||
+8
@@ -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>
|
||||||
-14
@@ -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>
|
|
||||||
@@ -150,10 +150,13 @@ Create **Publish to blog**:
|
|||||||
only `published` means the deployed site's receipt contains this album.
|
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
|
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
|
upload job. Each photo is stored as its own `_art` document and renders as its
|
||||||
batches. The caption appears on each photo; the first image is the highlight target. Metadata sidecars
|
own gallery `<li>`. The shared caption is repeated on each photo and an optional
|
||||||
under `__MACOSX/` and `.DS_Store` are ignored. Ordinary API clients can also send
|
highlight begins on the first photo. Photos remain contiguous in the selection order.
|
||||||
repeated `photos` parts; send either `archive` or `photos`, never both.
|
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
|
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.
|
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**,
|
An optional highlight name (up to 200 characters) adds one link to **Highlights**,
|
||||||
|
|||||||
+29
-15
@@ -10,7 +10,7 @@ from pathlib import Path
|
|||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
import warnings
|
import warnings
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
@@ -79,6 +79,26 @@ def web_image(raw, destination):
|
|||||||
Image.Resampling.LANCZOS)
|
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):
|
def create_app(data_dir=None, token=None):
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.update(MAX_CONTENT_LENGTH=100 * 1024 * 1024,
|
app.config.update(MAX_CONTENT_LENGTH=100 * 1024 * 1024,
|
||||||
@@ -261,21 +281,15 @@ def create_app(data_dir=None, token=None):
|
|||||||
if kind == "snippet":
|
if kind == "snippet":
|
||||||
manifest.update(kind=kind, text=text)
|
manifest.update(kind=kind, text=text)
|
||||||
atomic_json(temporary / "manifest.json", manifest)
|
atomic_json(temporary / "manifest.json", manifest)
|
||||||
content = ["---", "layout: post", f'title: "photo-{job}"',
|
if kind == "photo":
|
||||||
f'date: "{date}"', "categories: art"]
|
(temporary / "entries").mkdir()
|
||||||
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)):
|
for index in range(len(photos)):
|
||||||
content.append(f'<p><img src="/img/arts/uploads/{job}/{index:02}.jpg" '
|
(temporary / "entries" / f"{index:02}.md").write_text(
|
||||||
f'alt="{public_text(caption)}" loading="lazy"></p>')
|
photo_entry(job, date, index, len(photos), caption, highlight))
|
||||||
if caption:
|
# Retain the old filename for compatibility with publisher versions
|
||||||
content.append('<span class="image-details">' +
|
# that predate one-document-per-photo submissions.
|
||||||
public_text(caption).replace("\n", "<br>") + "</span>")
|
content = [(temporary / "entries/00.md").read_text()]
|
||||||
if kind == "snippet":
|
else:
|
||||||
content = [f'<div id="snippet-{job}"></div>', ""]
|
content = [f'<div id="snippet-{job}"></div>', ""]
|
||||||
for paragraph in (text, caption):
|
for paragraph in (text, caption):
|
||||||
if paragraph:
|
if paragraph:
|
||||||
|
|||||||
@@ -86,9 +86,20 @@ class Publisher:
|
|||||||
entry = Path("_art") / f"{manifest['date'][:10]}-photo-{job}.md"
|
entry = Path("_art") / f"{manifest['date'][:10]}-photo-{job}.md"
|
||||||
images = Path("img/arts/uploads") / job
|
images = Path("img/arts/uploads") / job
|
||||||
(self.repo / entry).parent.mkdir(parents=True, exist_ok=True)
|
(self.repo / entry).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
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)
|
shutil.copyfile(submission / "entry.md", self.repo / entry)
|
||||||
|
self.git("add", "--", str(entry))
|
||||||
shutil.copytree(submission / "images", self.repo / images, dirs_exist_ok=True)
|
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"):
|
if self.git("diff", "--cached", "--name-only"):
|
||||||
self.git("commit", "-m", f"Publish {len(jobs)} submission(s)")
|
self.git("commit", "-m", f"Publish {len(jobs)} submission(s)")
|
||||||
self.git("push", "origin", f"HEAD:refs/heads/{self.branch}")
|
self.git("push", "origin", f"HEAD:refs/heads/{self.branch}")
|
||||||
|
|||||||
@@ -116,10 +116,12 @@ def test_album_caption_order_and_retry_deduplication(service):
|
|||||||
assert response.status_code == 202
|
assert response.status_code == 202
|
||||||
job = response.json["id"]
|
job = response.json["id"]
|
||||||
folder = data / "submissions" / job
|
folder = data / "submissions" / job
|
||||||
entry = (folder / "entry.md").read_text()
|
entries = sorted((folder / "entries").glob("*.md"))
|
||||||
assert "<script>" not in entry and "{%" not in entry and "{{" not in entry
|
assert len(entries) == 2
|
||||||
assert "<br>second line" in entry
|
rendered = "\n".join(entry.read_text() for entry in entries)
|
||||||
assert entry.index("00.jpg") < entry.index("01.jpg")
|
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/00.jpg").getpixel((0, 0))[0] > 240
|
||||||
assert Image.open(folder / "images/01.jpg").getpixel((0, 0))[2] > 240
|
assert Image.open(folder / "images/01.jpg").getpixel((0, 0))[2] > 240
|
||||||
assert (folder / "originals/0").read_bytes() == images[0]
|
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
|
assert response.status_code == 202
|
||||||
job = response.json["id"]
|
job = response.json["id"]
|
||||||
folder = data / "submissions" / job
|
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:]
|
metadata, body = entry.split("---\n", 2)[1:]
|
||||||
anchor = next(line for line in metadata.splitlines() if line.startswith("anchor: "))
|
anchor = next(line for line in metadata.splitlines() if line.startswith("anchor: "))
|
||||||
assert json.loads(anchor.removeprefix("anchor: ")) == name
|
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.
|
# The template emits the sole target at the start of this album, before 00.jpg.
|
||||||
assert f'title: "photo-{job}"' in metadata
|
assert f'title: "photo-{job}"' in metadata
|
||||||
assert f'id="photo-{job}"' not in body
|
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 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=name).status_code == 200
|
||||||
assert upload(client, images, "Album caption", highlight="Other trip").json["id"] != job
|
assert upload(client, images, "Album caption", highlight="Other trip").json["id"] != job
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -4,7 +4,7 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
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({
|
Path('_site/photo-publication.json').write_text(json.dumps({
|
||||||
'commit': os.environ['GITHUB_SHA'], 'photos': photos,
|
'commit': os.environ['GITHUB_SHA'], 'photos': photos,
|
||||||
'snippets': sorted(set(re.findall(r'id="snippet-([a-f0-9]{64})"',
|
'snippets': sorted(set(re.findall(r'id="snippet-([a-f0-9]{64})"',
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user