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() 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: