Date photo uploads from capture metadata
This commit is contained in:
@@ -26,6 +26,10 @@ def service(tmp_path):
|
||||
|
||||
|
||||
def photo(color="red", format="JPEG", size=(40, 20), exif=None):
|
||||
if exif is None:
|
||||
exif = Image.Exif()
|
||||
exif[36867] = "2025:01:02 03:04:05"
|
||||
exif[36881] = "+00:00"
|
||||
output = io.BytesIO()
|
||||
Image.new("RGB", size, color).save(output, format, **({"exif": exif} if exif else {}))
|
||||
return output.getvalue()
|
||||
@@ -64,8 +68,10 @@ def test_validation_is_all_or_nothing(service):
|
||||
def test_multi_megabyte_multipart_file(service):
|
||||
_, client, data = service
|
||||
output = io.BytesIO()
|
||||
exif = Image.Exif()
|
||||
exif[36867] = "2025:01:02 03:04:05"
|
||||
Image.frombytes("RGB", (1800, 1800), random.Random(2).randbytes(1800 * 1800 * 3)).save(
|
||||
output, "JPEG", quality=95)
|
||||
output, "JPEG", quality=95, exif=exif)
|
||||
assert len(output.getvalue()) > 3 * 1024 * 1024
|
||||
response = upload(client, [output.getvalue()])
|
||||
assert response.status_code == 202, response.json
|
||||
@@ -138,6 +144,7 @@ def test_orientation_metadata_and_heic(service):
|
||||
exif = Image.Exif()
|
||||
exif[274] = 6
|
||||
exif[270] = "private metadata"
|
||||
exif[36867] = "2025:01:02 03:04:05"
|
||||
for raw in [photo(exif=exif, size=(3000, 1500)), photo(format="HEIF")]:
|
||||
response = upload(client, [raw])
|
||||
assert response.status_code == 202
|
||||
@@ -149,6 +156,61 @@ def test_orientation_metadata_and_heic(service):
|
||||
assert image.height > image.width
|
||||
|
||||
|
||||
def test_photo_date_comes_from_original_metadata(service):
|
||||
_, client, data = service
|
||||
exif = Image.Exif()
|
||||
exif[36867] = "2025:04:18 14:23:11"
|
||||
exif[36881] = "+02:00"
|
||||
|
||||
response = upload(client, [photo(exif=exif)])
|
||||
|
||||
assert response.status_code == 202
|
||||
entry = (data / "submissions" / response.json["id"] / "entries/00.md").read_text()
|
||||
assert 'date: "2025-04-18T14:23:11+02:00"' in entry
|
||||
|
||||
|
||||
def test_photo_dates_can_come_from_photos_library_metadata(service):
|
||||
_, client, data = service
|
||||
response = client.post("/api/photos", headers=AUTH, data={
|
||||
"photos": [
|
||||
(io.BytesIO(photo("red")), "first.jpg"),
|
||||
(io.BytesIO(photo("blue")), "second.jpg"),
|
||||
],
|
||||
"photo_dates": "2025-04-18T14:23:11+02:00\n2025-04-19T09:10:05+02:00",
|
||||
})
|
||||
assert response.status_code == 202
|
||||
entries = sorted((data / "submissions" / response.json["id"] / "entries").glob("*.md"))
|
||||
assert 'date: "2025-04-18T14:23:11+02:00"' in entries[0].read_text()
|
||||
assert 'date: "2025-04-19T09:10:05+02:00"' in entries[1].read_text()
|
||||
|
||||
invalid = client.post("/api/photos", headers=AUTH, data={
|
||||
"photos": [(io.BytesIO(photo()), "photo.jpg")],
|
||||
"photo_dates": "not-a-date",
|
||||
})
|
||||
assert invalid.status_code == 400
|
||||
assert "valid ISO dates" in invalid.json["error"]
|
||||
|
||||
missing = upload(client, [photo(exif=Image.Exif())])
|
||||
assert missing.status_code == 400
|
||||
assert "capture date is missing" in missing.json["error"]
|
||||
|
||||
|
||||
def test_dated_retry_replaces_the_same_undated_upload(service):
|
||||
_, client, data = service
|
||||
raw = photo()
|
||||
original = upload(client, [raw], "Same caption", highlight="Trip").json
|
||||
replacement = client.post("/api/photos", headers=AUTH, data={
|
||||
"photos": [(io.BytesIO(raw), "photo.jpg")],
|
||||
"caption": "Same caption",
|
||||
"highlight": "Trip",
|
||||
"photo_dates": "2025-04-18T14:23:11+02:00",
|
||||
}).json
|
||||
|
||||
assert replacement["id"] != original["id"]
|
||||
manifest = json.loads((data / "submissions" / replacement["id"] / "manifest.json").read_text())
|
||||
assert manifest["replaces"] == original["id"]
|
||||
|
||||
|
||||
def test_optional_highlight_preserves_existing_upload_ids(service):
|
||||
_, client, data = service
|
||||
raw = photo()
|
||||
@@ -239,6 +301,40 @@ def test_git_push_and_crash_recovery(publisher, service, monkeypatch, tmp_path):
|
||||
assert client.get(response.json["status_url"], headers=AUTH).json["status"] == "published"
|
||||
|
||||
|
||||
def test_publisher_names_photo_entry_from_capture_date(publisher, service):
|
||||
run, remote, client = publisher
|
||||
exif = Image.Exif()
|
||||
exif[36867] = "2025:04:18 14:23:11"
|
||||
response = upload(client, [photo(exif=exif)])
|
||||
|
||||
result = run()
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
paths = git(remote, "ls-tree", "-r", "--name-only", "master")
|
||||
assert f'_art/2025-04-18-photo-{response.json["id"]}-00.md' in paths
|
||||
|
||||
|
||||
def test_publisher_replaces_an_undated_version_of_the_same_photos(publisher, service):
|
||||
run, remote, client = publisher
|
||||
raw = photo()
|
||||
original = upload(client, [raw], "Same caption", highlight="Trip").json
|
||||
assert run().returncode == 0
|
||||
replacement = client.post("/api/photos", headers=AUTH, data={
|
||||
"photos": [(io.BytesIO(raw), "photo.jpg")],
|
||||
"caption": "Same caption",
|
||||
"highlight": "Trip",
|
||||
"photo_dates": "2025-04-18T14:23:11+02:00",
|
||||
}).json
|
||||
|
||||
result = run()
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
paths = git(remote, "ls-tree", "-r", "--name-only", "master")
|
||||
assert f"photo-{original['id']}" not in paths
|
||||
assert f"img/arts/uploads/{original['id']}" not in paths
|
||||
assert f'_art/2025-04-18-photo-{replacement["id"]}-00.md' in paths
|
||||
|
||||
|
||||
def test_rejected_push_can_retry(publisher):
|
||||
run, remote, client = publisher
|
||||
head = git(remote, "rev-parse", "master")
|
||||
|
||||
Reference in New Issue
Block a user