From feadaec7c5f6baaf909a97cb4ee00904abb2f8a1 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:34:47 -0500 Subject: [PATCH 1/2] fix(web-portal): handle escape characters --- specifyweb/backend/stored_queries/web_portal_export.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/specifyweb/backend/stored_queries/web_portal_export.py b/specifyweb/backend/stored_queries/web_portal_export.py index 4180ef9f105..e5f5991c8ff 100644 --- a/specifyweb/backend/stored_queries/web_portal_export.py +++ b/specifyweb/backend/stored_queries/web_portal_export.py @@ -81,7 +81,11 @@ def _schema_localization_or_empty(collection) -> dict[str, Any]: def _clean_cell(value: Any) -> str: - return re.sub("\r|\n", " ", str(value if value is not None else '')) + # The portal unescapes Java-style sequences in exported text. See #8644! + # Escape literal sequences such as ``\\r`` by escaping their backslash before + # the value is written to CSV. + text = str(value if value is not None else '') + return re.sub("\r|\n", " ", text).replace('\\', '\\\\') def _clean_portal_attachment_text(value: Any) -> str: From 68c0c1f6ca68c9d3aaf402f9330a9e1d609500d9 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:35:03 -0500 Subject: [PATCH 2/2] test(web-portal): check escape characters --- .../tests/test_views/test_export_web_portal.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/specifyweb/backend/stored_queries/tests/test_views/test_export_web_portal.py b/specifyweb/backend/stored_queries/tests/test_views/test_export_web_portal.py index 2a1505b7761..0452a93e31a 100644 --- a/specifyweb/backend/stored_queries/tests/test_views/test_export_web_portal.py +++ b/specifyweb/backend/stored_queries/tests/test_views/test_export_web_portal.py @@ -8,6 +8,15 @@ class TestExportWebPortal(SQLAlchemySetup): + def test_clean_cell_preserves_literal_java_escape_sequences(self): + from specifyweb.backend.stored_queries.web_portal_export import _clean_cell + + self.assertEqual( + _clean_cell(r"literal \r, \b, \f and \\"), + r"literal \\r, \\b, \\f and \\\\", + ) + self.assertEqual(_clean_cell("line\nbreak\r"), "line break ") + @patch("specifyweb.backend.stored_queries.views.Thread") def test_export(self, thread: Mock): c = Client()