diff --git a/CHANGELOG.md b/CHANGELOG.md index ef245b1..04df7a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to Splat Workshop. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versions follow [Semantic Versioning](https://semver.org/). +## [Unreleased] + +### Fixed + +- **Folder names without a dot at the end**: a name like "Tour 12.10." gave the folder `Tour-12.10.`, which Windows file systems (NTFS, e.g. a second disk the runs are moved to) refuse – the copy silently lacked that tour. Now it is `Tour-12.10`. Existing folders keep their names. + ## [0.3.0] – 2026-10-15 ### Added diff --git a/splat_workshop/config.py b/splat_workshop/config.py index c319a92..d81a7f5 100644 --- a/splat_workshop/config.py +++ b/splat_workshop/config.py @@ -76,7 +76,8 @@ class Config: def slugify(name: str) -> str: - """Folder/file name from a display name: ä→ae etc., accents dropped, only [A-Za-z0-9._-].""" + """Folder/file name from a display name: ä→ae etc., accents dropped, only [A-Za-z0-9._-]; no dot or + dash at the ends ("Tour 12.10." → Tour-12.10: Windows file systems such as NTFS refuse a trailing dot).""" for a, b in (("ä", "ae"), ("ö", "oe"), ("ü", "ue"), ("Ä", "Ae"), ("Ö", "Oe"), ("Ü", "Ue"), ("ß", "ss")): name = name.replace(a, b) name = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode() @@ -88,7 +89,7 @@ def slugify(name: str) -> str: elif not dash: out.append("-") dash = True - return "".join(out).strip("-") + return "".join(out).strip("-.") def _merge(base: dict, over: dict) -> dict: diff --git a/tests/test_groups.py b/tests/test_groups.py index bc22e78..58bc8fa 100644 --- a/tests/test_groups.py +++ b/tests/test_groups.py @@ -1,5 +1,6 @@ """Groups of scenes: one group per scene, suggestions, renaming, cleaning up together.""" import json +import pytest from conftest import RUNS, server import synth @@ -78,3 +79,18 @@ def test_dissolving_a_group_keeps_its_scenes(api): assert api.post("/api/delete", {"slug": "Home"})[0] == 200 assert api.get("/api/groups")[1]["groups"] == [] assert {"Kitchen", "Hallway", "Statue"} <= {s["slug"] for s in server.scenes()} + + +@pytest.mark.parametrize("name,slug", [("Rundgang 12.10.", "Rundgang-12.10"), ("Küche", "Kueche"), + ("...", ""), (".hidden", "hidden"), ("Flur 2", "Flur-2")]) +def test_folder_names_suit_every_file_system(name, slug): + """No dot at the end: NTFS (e.g. runs on a Windows disk) refuses it, and the copy loses the tour.""" + from splat_workshop import config + assert config.slugify(name) == slug + + +def test_a_tour_named_with_a_date_gets_a_folder_ntfs_accepts(api): + synth.make_room(RUNS, "Kitchen", "A", name="Kitchen") + synth.make_room(RUNS, "Hall", "B", name="Hall") + status, r = api.post("/api/group", {"name": "Tour 12.10.", "members": "Kitchen,Hall"}) + assert status == 200 and r["slug"] == "Tour-12.10" diff --git a/tests/test_scenes.py b/tests/test_scenes.py index 9dca7f7..518db0d 100644 --- a/tests/test_scenes.py +++ b/tests/test_scenes.py @@ -23,13 +23,13 @@ def test_renaming_moves_folder_files_and_log(api): (run / "aufnahme-12.10.-16-40.gallery.png").write_bytes(b"x") status, r = api.post("/api/rename", {"slug": "Aufnahme-12.10.-16-40", "new": "Büro 12.10."}) assert status == 200, r - new = RUNS / "Buero-12.10." + new = RUNS / "Buero-12.10" assert new.is_dir() and not run.exists() - assert (new / "buero-12.10..sog").exists() and (new / "buero-12.10..gallery.png").exists() - assert (RUNS / "Buero-12.10..log").exists() and not (RUNS / "Aufnahme-12.10.-16-40.log").exists() - meta = json.loads((RUNS / "Buero-12.10..ui.json").read_text()) + assert (new / "buero-12.10.sog").exists() and (new / "buero-12.10.gallery.png").exists() + assert (RUNS / "Buero-12.10.log").exists() and not (RUNS / "Aufnahme-12.10.-16-40.log").exists() + meta = json.loads((RUNS / "Buero-12.10.ui.json").read_text()) assert meta["name"] == "Büro 12.10." and meta["folder_renamed"] is True - assert server.no_resume("Buero-12.10.") == "renamed" # the pipeline remembers the old path + assert server.no_resume("Buero-12.10") == "renamed" # the pipeline remembers the old path def test_renaming_to_a_taken_name_is_refused(api):