From ecfa0d7dcaf514844ef594c0674e28d9a9d36314 Mon Sep 17 00:00:00 2001 From: Dan Birman Date: Mon, 2 Dec 2024 12:39:04 -0800 Subject: [PATCH] refactor: improving utils, fixing minor bugs, adding test coverage --- src/aind_qc_portal/panel/media.py | 7 +- src/aind_qc_portal/panel/metric.py | 2 +- src/aind_qc_portal/panel/quality_control.py | 3 - src/aind_qc_portal/qc_asset_app.py | 2 - src/aind_qc_portal/qc_portal_app.py | 2 - src/aind_qc_portal/utils.py | 39 ++++------- tests/test_utils.py | 74 +++++++++++++++++++++ 7 files changed, 91 insertions(+), 38 deletions(-) create mode 100644 tests/test_utils.py diff --git a/src/aind_qc_portal/panel/media.py b/src/aind_qc_portal/panel/media.py index cb836b2..acf31f4 100644 --- a/src/aind_qc_portal/panel/media.py +++ b/src/aind_qc_portal/panel/media.py @@ -52,7 +52,7 @@ class Fullscreen(ReactiveHTML): """A Fullscreen component that allows the user to toggle fullscreen mode. """ - + object = param.Parameter() def __init__(self, object, **params): @@ -176,9 +176,6 @@ def panel(self): return Fullscreen(self.object, sizing_mode="stretch_width", max_height=1200) -def _iframe_html(reference): - return f'' - def _is_image(reference): return reference.endswith(".png") or reference.endswith(".jpg") or reference.endswith(".gif") or reference.endswith(".jpeg") or reference.endswith(".svg") or reference.endswith(".pdf") @@ -229,7 +226,7 @@ def _parse_type(reference, data): elif "neuroglancer" in reference: iframe_html = f'' return pn.pane.HTML( - iframe_html, sizing_mode="stretch_both" + iframe_html, sizing_mode="stretch_width", min_height=1000, max_height=1200 ) elif "http" in reference: return pn.widgets.StaticText( diff --git a/src/aind_qc_portal/panel/metric.py b/src/aind_qc_portal/panel/metric.py index 519dc63..2cf0e9e 100644 --- a/src/aind_qc_portal/panel/metric.py +++ b/src/aind_qc_portal/panel/metric.py @@ -66,7 +66,7 @@ def _set_status(self, status: Status | str): self._data.status_history.append( QCStatus( - evaluator=f"{pn.state.user_info['given_name']} {pn.state.user_info['family_name']}", + evaluator=f"{pn.state.user_info.get("given_name", "")} {pn.state.user_info.get("family_name", "")}", status=status, timestamp=datetime.now(), ) diff --git a/src/aind_qc_portal/panel/quality_control.py b/src/aind_qc_portal/panel/quality_control.py index 44a3047..6b16a61 100644 --- a/src/aind_qc_portal/panel/quality_control.py +++ b/src/aind_qc_portal/panel/quality_control.py @@ -11,7 +11,6 @@ from aind_qc_portal.panel.evaluation import QCEvalPanel from aind_qc_portal.utils import ( status_html, - update_schema_version, OUTER_STYLE, ) @@ -89,8 +88,6 @@ def get_data(self): self.s3_bucket = s3_location.split("/")[0] self.s3_prefix = s3_location.split("/")[1] - update_schema_version(json_data) - self.asset_name = json_data["name"] try: self._data = QualityControl.model_validate_json( diff --git a/src/aind_qc_portal/qc_asset_app.py b/src/aind_qc_portal/qc_asset_app.py index 09a11e6..9d0f05b 100644 --- a/src/aind_qc_portal/qc_asset_app.py +++ b/src/aind_qc_portal/qc_asset_app.py @@ -16,7 +16,6 @@ QC_LINK_PREFIX, df_timestamp_range, qc_color, - update_schema_version, AIND_COLORS, OUTER_STYLE, set_background, @@ -67,7 +66,6 @@ def parse_records(self): # [TODO] this is designed as-is because the current metadata records are all missing the input_data_name field, unfortunately for record in self._records: - record = update_schema_version(record) name_split = record["name"].split("_") diff --git a/src/aind_qc_portal/qc_portal_app.py b/src/aind_qc_portal/qc_portal_app.py index 81fa499..af96e59 100644 --- a/src/aind_qc_portal/qc_portal_app.py +++ b/src/aind_qc_portal/qc_portal_app.py @@ -17,7 +17,6 @@ ASSET_LINK_PREFIX, QC_LINK_PREFIX, qc_color, - update_schema_version, OUTER_STYLE, AIND_COLORS, set_background, @@ -47,7 +46,6 @@ def __init__(self): if "quality_control" in record and record["quality_control"]: # try to validate the QC object - record = update_schema_version(record) try: qc = QualityControl.model_validate_json( diff --git a/src/aind_qc_portal/utils.py b/src/aind_qc_portal/utils.py index 8c0bfb0..8a439e4 100644 --- a/src/aind_qc_portal/utils.py +++ b/src/aind_qc_portal/utils.py @@ -26,21 +26,6 @@ "margin": "5px", } -background_color = AIND_COLORS[ - ( - pn.state.location.query_params["background"] - if "background" in pn.state.location.query_params - else "dark_blue" - ) -] -BACKGROUND_CSS = f""" -body {{ - background-color: {background_color} !important; - background-image: url('/images/aind-pattern.svg') !important; - background-size: 1200px; -}} -""" - def format_link(link: str, text: str = "link"): """Format link as an HTML anchor tag @@ -57,19 +42,23 @@ def format_link(link: str, text: str = "link"): def set_background(): """Set the background color of the Panel app""" # Add the custom CSS + background_color = AIND_COLORS[ + ( + pn.state.location.query_params["background"] + if "background" in pn.state.location.query_params + else "dark_blue" + ) + ] + BACKGROUND_CSS = f""" + body {{ + background-color: {background_color} !important; + background-image: url('/images/aind-pattern.svg') !important; + background-size: 1200px; + }} + """ pn.config.raw_css.append(BACKGROUND_CSS) -def update_schema_version(record: dict): - """Update the schema version in the quality control field""" - if record.get("quality_control") and record.get("schema_version"): - record["quality_control"][ - "schema_version" - ] = QualityControl.model_construct().schema_version - - return record - - def status_html(status: Status): if status.value == "Pass": color = AIND_COLORS["green"] diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..1ecaf74 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,74 @@ +import unittest +from datetime import datetime +from unittest.mock import patch +import pandas as pd +import numpy as np +from aind_data_schema.core.quality_control import Status + +from aind_qc_portal.utils import ( + format_link, + set_background, + status_html, + df_timestamp_range, + md_style, + qc_color, + bincount2D +) + + +class TestUtils(unittest.TestCase): + + def test_format_link(self): + self.assertEqual(format_link("http://example.com"), 'link') + self.assertEqual(format_link("http://example.com", "Example"), 'Example') + + def test_status_html(self): + self.assertEqual(status_html(Status.PASS), 'Pass') + self.assertEqual(status_html(Status.PENDING), 'Pending') + self.assertEqual(status_html(Status.FAIL), 'Fail') + + def test_df_timestamp_range(self): + data = { + "timestamp": [ + datetime(2023, 1, 1), + datetime(2023, 1, 2), + datetime(2023, 1, 3), + ] + } + df = pd.DataFrame(data) + min_range, max_range, unit, fmt = df_timestamp_range(df) + self.assertEqual(unit, "day") + self.assertEqual(fmt, "%b %d") + + def test_md_style(self): + self.assertEqual(md_style(12, "test"), 'test') + + def test_qc_color(self): + self.assertEqual(qc_color("No QC"), "background-color: #FFB71B") + self.assertEqual(qc_color("Pass"), "background-color: #1D8649") + self.assertEqual(qc_color("Fail"), "background-color: #FF5733") + self.assertEqual(qc_color("Pending"), "background-color: #2A7DE1") + + def test_bincount2D(self): + x = np.array([1, 2, 2, 3]) + y = np.array([4, 5, 5, 6]) + r, xscale, yscale = bincount2D(x, y) + self.assertEqual(r.shape, (3, 3)) + self.assertTrue(np.array_equal(xscale, np.array([1, 2, 3]))) + self.assertTrue(np.array_equal(yscale, np.array([4, 5, 6]))) + + @patch('aind_qc_portal.utils.pn') + def test_set_background(self, mock_pn): + mock_pn.config.raw_css = [] # Mock raw_css to ensure a clean state + mock_pn.state.location.query_params = {} # Mock query_params to ensure no background param + set_background() + self.assertIn("background-color: #003057", mock_pn.config.raw_css[0]) # Default dark_blue color + + mock_pn.config.raw_css = [] # Reset mock raw_css + mock_pn.state.location.query_params = {"background": "light_blue"} # Mock query_params with light_blue + set_background() + self.assertIn("background-color: #2A7DE1", mock_pn.config.raw_css[0]) # light_blue color + + +if __name__ == "__main__": + unittest.main()