Organize gallery into highlighted photo groups
This commit is contained in:
@@ -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
@@ -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:
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user