A host from the web site; the same page kept once; deleting from the Notebooks chooser
From the development repository at 299f73d, 2026-10-11: hosting from the site documented; a page loaded from a notebook and left unchanged is not kept again; notebooks and pages are deleted from the chooser, to the system's Trash (--trash); the Notebooks button. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
4e3a281d65
commit
0d01e23d7c
15 files changed
+711
-63
No files matched your search
@@ -28,14 +28,29 @@ TEXT_ONLY_LAYER = (b'<g data-layer="abc" data-name="Andy"><g data-text="t1" data
|
||||
WRITTEN_PAGE = b'<svg data-kokkoja="1"><g data-layer="a"><g data-stroke="s"/></g></svg>\n'
|
||||
|
||||
|
||||
def page_named(session, page, number, body=b'<g data-layer="a"><g data-stroke="s"/></g>'):
|
||||
"""A page file as the client writes it: named on the root element and
|
||||
in the title, then the paper and the content."""
|
||||
return (b'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
b'<svg xmlns="http://www.w3.org/2000/svg" data-kokkoja="1" data-session="' + session +
|
||||
b'" data-host="h" data-page="' + page + b'" data-number="' + number +
|
||||
b'" data-base="2970 2100" viewBox="0 0 2970 2100">\n'
|
||||
b'<title>Kokkoja page ' + number + b'</title>\n'
|
||||
b'<rect x="0" y="0" width="2970" height="2100" fill="#ffffff"/>\n' + body + b'</svg>\n')
|
||||
|
||||
|
||||
class LocalProgramTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.directory = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.directory.cleanup)
|
||||
self.notebooks = pathlib.Path(self.directory.name) / "notebooks"
|
||||
self.config = pathlib.Path(self.directory.name) / "config"
|
||||
# Deleted things go to a Trash of the test's own, a plain
|
||||
# directory as macOS's Trash is; the freedesktop layout has its
|
||||
# own test.
|
||||
self.trash = pathlib.Path(self.directory.name) / "trash"
|
||||
self.server = kokkoja.make_server(0, "ws://relay.example:8770/", self.notebooks, TOKEN,
|
||||
self.config)
|
||||
self.config, trash=kokkoja.Trash(self.trash))
|
||||
self.port = self.server.server_address[1]
|
||||
thread = threading.Thread(target=self.server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
@@ -166,6 +181,83 @@ class LocalProgramTest(unittest.TestCase):
|
||||
self.request("PUT", current, body=b'<svg data-kokkoja="1">' + TEXT_ONLY_LAYER + b"</svg>\n")
|
||||
self.assertEqual(self.json("POST", keep), (200, {"kept": f"{today}_03.svg", "new": True}))
|
||||
|
||||
# The same page under other names is the same page: loaded from
|
||||
# the notebook into another session, it is a new page with a new
|
||||
# id and number, and is not kept again when left unchanged. A
|
||||
# different sheet, or typed text that happens to name a page, is
|
||||
# not the same.
|
||||
self.request("PUT", current, body=page_named(b"s1", b"p1", b"4"))
|
||||
self.assertEqual(self.json("POST", keep), (200, {"kept": f"{today}_04.svg", "new": True}))
|
||||
self.request("PUT", current, body=page_named(b"s2", b"p2", b"11"))
|
||||
self.assertEqual(self.json("POST", keep), (200, {"kept": f"{today}_04.svg", "new": False}))
|
||||
self.request("PUT", current, body=page_named(b"s2", b"p2", b"11").replace(b'viewBox="0 0', b'viewBox="-50 0'))
|
||||
self.assertEqual(self.json("POST", keep), (200, {"kept": f"{today}_05.svg", "new": True}))
|
||||
typed = page_named(b"s1", b"p1", b"4", body=b'<g data-layer="a"><g data-text="t"><desc>data-page="p1"</desc></g></g>')
|
||||
self.request("PUT", current, body=typed)
|
||||
self.assertEqual(self.json("POST", keep), (200, {"kept": f"{today}_06.svg", "new": True}))
|
||||
self.request("PUT", current, body=typed.replace(b'<desc>data-page="p1"', b'<desc>data-page="p2"'))
|
||||
self.assertEqual(self.json("POST", keep), (200, {"kept": f"{today}_07.svg", "new": True}))
|
||||
|
||||
def test_deleting_pages_and_notebooks(self):
|
||||
notebook = self.notebooks / "Andy"
|
||||
notebook.mkdir(parents=True)
|
||||
for name in ("Current.svg", "2026_10_01_01.svg", "2026_10_01_02.svg"):
|
||||
(notebook / name).write_bytes(WRITTEN_PAGE)
|
||||
(self.notebooks / "Other").mkdir()
|
||||
(self.notebooks / "Other" / "2026_10_02_01.svg").write_bytes(WRITTEN_PAGE)
|
||||
self.assertTrue(self.json("GET", "/api/notebooks")[1]["trash"])
|
||||
|
||||
# A kept page goes to the Trash; the page being written stays.
|
||||
self.assertEqual(self.json("DELETE", "/api/notebooks/Andy/2026_10_01_01.svg"),
|
||||
(200, {"discarded": "2026_10_01_01.svg", "trash": True}))
|
||||
self.assertEqual(self.json("GET", "/api/notebooks/Andy")[1]["pages"], ["2026_10_01_02.svg", "Current.svg"])
|
||||
self.assertEqual((self.trash / "2026_10_01_01.svg").read_bytes(), WRITTEN_PAGE)
|
||||
self.assertEqual(self.request("DELETE", "/api/notebooks/Andy/Current.svg")[0], 400)
|
||||
self.assertEqual(self.request("DELETE", "/api/notebooks/Andy/none.svg")[0], 404)
|
||||
self.assertEqual(self.request("DELETE", "/api/notebooks")[0], 405)
|
||||
self.assertEqual(self.request("DELETE", "/api/notebooks/Andy/2026_10_01_02.svg", token=None)[0], 401)
|
||||
|
||||
# A whole notebook goes as one, and a name already taken in the
|
||||
# Trash is not overwritten.
|
||||
(self.notebooks / "Andy" / "2026_10_01_01.svg").write_bytes(b"kept again")
|
||||
self.assertEqual(self.json("DELETE", "/api/notebooks/Andy"),
|
||||
(200, {"discarded": "Andy", "trash": True}))
|
||||
self.assertEqual(self.json("GET", "/api/notebooks")[1]["notebooks"], ["Other"])
|
||||
self.assertEqual(sorted(p.name for p in (self.trash / "Andy").iterdir()),
|
||||
["2026_10_01_01.svg", "2026_10_01_02.svg", "Current.svg"])
|
||||
(self.notebooks / "Andy").mkdir()
|
||||
(self.notebooks / "Andy" / "Current.svg").write_bytes(b"a second Andy")
|
||||
self.assertEqual(self.json("DELETE", "/api/notebooks/Andy")[1]["discarded"], "Andy")
|
||||
self.assertEqual((self.trash / "Andy.2" / "Current.svg").read_bytes(), b"a second Andy")
|
||||
self.assertEqual((self.trash / "2026_10_01_01.svg").read_bytes(), WRITTEN_PAGE)
|
||||
|
||||
def test_the_freedesktop_trash(self):
|
||||
basket = pathlib.Path(self.directory.name) / "Trash"
|
||||
trash = kokkoja.Trash(basket, freedesktop=True)
|
||||
page = self.notebooks / "Andy" / "2026_10_01_01.svg"
|
||||
page.parent.mkdir(parents=True)
|
||||
page.write_bytes(WRITTEN_PAGE)
|
||||
trash.take(page)
|
||||
self.assertFalse(page.exists())
|
||||
self.assertEqual((basket / "files" / page.name).read_bytes(), WRITTEN_PAGE)
|
||||
info = (basket / "info" / f"{page.name}.trashinfo").read_text()
|
||||
self.assertTrue(info.startswith("[Trash Info]\nPath="), info)
|
||||
self.assertIn(f"Path={urllib.parse.quote(str(page.resolve()))}\n", info)
|
||||
self.assertRegex(info, r"DeletionDate=\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\n")
|
||||
self.assertEqual(basket.stat().st_mode & 0o777, 0o700)
|
||||
# The same name again: the info file reserves the first, so the
|
||||
# second gets a serial.
|
||||
page.write_bytes(b"another")
|
||||
trash.take(page)
|
||||
self.assertEqual((basket / "files" / "2026_10_01_01.2.svg").read_bytes(), b"another")
|
||||
self.assertTrue((basket / "info" / "2026_10_01_01.2.svg.trashinfo").exists())
|
||||
# Without a Trash, a deletion is for good.
|
||||
page.write_bytes(b"gone")
|
||||
self.assertFalse(kokkoja.discard(page, None))
|
||||
self.assertFalse(page.exists())
|
||||
self.assertTrue(kokkoja.discard(page.parent, kokkoja.Trash(basket)))
|
||||
self.assertTrue((basket / "Andy").is_dir())
|
||||
|
||||
def test_a_kept_page_is_dated_when_it_was_last_changed(self):
|
||||
notebook = self.notebooks / "Andy"
|
||||
notebook.mkdir(parents=True)
|
||||
|
||||
Reference in new issue
Block a user