diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py index 45f276af..5dba8507 100644 --- a/src/openflexure_microscope_server/things/camera/__init__.py +++ b/src/openflexure_microscope_server/things/camera/__init__.py @@ -298,8 +298,8 @@ class BaseCamera(OFMThing, ABC): """The schema (BaseModel) for passing data to the gallery.""" return CaptureInfo - def get_data_for_gallery(self) -> list[CaptureInfo]: - """Return all the information about the saved captures.""" + def _all_captures(self) -> list[str]: + """Return the full path for all captures on disk.""" files = os.listdir(self._data_dir) captures = [] for filename in files: @@ -308,16 +308,31 @@ class BaseCamera(OFMThing, ABC): BASE_IMAGE_FORMATS["jpeg"].path_matches(filename) or BASE_IMAGE_FORMATS["png"].path_matches(filename) ): - captures.append( - CaptureInfo( - name=filename, - created=os.path.getctime(full_path), - modified=os.path.getmtime(full_path), - thing=self.name, - ) - ) + captures.append(full_path) return captures + def get_data_for_gallery(self) -> list[CaptureInfo]: + """Return all the information about the saved captures.""" + return [ + CaptureInfo( + name=os.path.basename(capture), + created=os.path.getctime(capture), + modified=os.path.getmtime(capture), + thing=self.name, + ) + for capture in self._all_captures() + ] + + def delete_all_gallery_items(self) -> None: + """Delete all the captures on the microscope. + + Use with extreme caution. + """ + for capture in self._all_captures(): + lt.raise_if_cancelled() + self.logger.info(f"Deleting: {capture}") + os.remove(capture) + @lt.endpoint( "delete", "capture/{name}", diff --git a/src/openflexure_microscope_server/things/gallery.py b/src/openflexure_microscope_server/things/gallery.py index 88a6eaed..c762943a 100644 --- a/src/openflexure_microscope_server/things/gallery.py +++ b/src/openflexure_microscope_server/things/gallery.py @@ -36,6 +36,8 @@ class GalleryCompatibleThing(Protocol): # Ignore D102: No docstrings for the protocol. def get_data_for_gallery(self) -> list[BaseModel]: ... # noqa: D102 + def delete_all_gallery_items(self) -> None: ... # noqa: D102 + class GalleryThing(lt.Thing): """A Thing for communicating with the front end gallery.""" @@ -95,5 +97,18 @@ class GalleryThing(lt.Thing): """ data_list = [] for thing in self.gallery_providing_things.values(): - data_list += [model.model_dump() for model in thing.get_data_for_gallery()] + try: + data_list += [ + model.model_dump() for model in thing.get_data_for_gallery() + ] + except Exception as e: + # If any of the providers errors producing a list, log the exception + # and continue. + self.logger.exception(e) return data_list + + @lt.action + def delete_all_data(self) -> None: + """Delete all the gallery data on this microscope.""" + for thing in self.gallery_providing_things.values(): + thing.delete_all_gallery_items() diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 328a76cb..e51c84eb 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -192,6 +192,18 @@ class SmartScanThing(OFMThing): ongoing=ongoing_name, include_ongoing=False ) + def delete_all_gallery_items(self) -> None: + """Delete all the scans on the microscope. + + **This will irreversibly remove all scanned data from the microscope!** + + Use with extreme caution. + """ + for scan_name in self._scan_dir_manager.all_scans: + lt.raise_if_cancelled() + self.logger.info(f"Deleting: {scan_name}") + self._delete_scan(scan_name) + # Note that the default detector name is set at init. This is over written if # setting is loaded from disk. @lt.setting @@ -608,20 +620,6 @@ class SmartScanThing(OFMThing): if not deleted_scan_success: raise HTTPException(400, "Couldn't delete scan, check log for details") - @lt.endpoint( - "delete", - "scans", - ) - def delete_all_scans(self) -> None: - """Delete all the scans on the microscope. - - **This will irreversibly remove all scanned data from the - microscope!** - Use with extreme caution. - """ - for scan_name in self._scan_dir_manager.all_scans: - self._delete_scan(scan_name) - @lt.action def purge_empty_scans(self) -> None: """Delete all scan folders containing no images at the top level.""" diff --git a/tests/unit_tests/test_gallery.py b/tests/unit_tests/test_gallery.py index 4e4eb088..db134262 100644 --- a/tests/unit_tests/test_gallery.py +++ b/tests/unit_tests/test_gallery.py @@ -36,6 +36,9 @@ class MinimalGalleryClass: """Return a list of Mock Gallery Data.""" return [MockGalleryData(mock="mock", foobar="foobar")] + def delete_all_gallery_items(self) -> None: + """Mock deleting all the data.""" + class MinimalGallerySource(OFMThing, MinimalGalleryClass): """Minimal example of a Thing that can show data in the gallery. @@ -60,6 +63,9 @@ class BadGallerySource(OFMThing): """Return a list of Mock Gallery Data.""" return [MockGalleryData(mock="mock", foobar="foobar")] + def delete_all_gallery_items(self) -> None: + """Mock deleting all the data.""" + def test_gallery_compatible_protocol(): """Check the gallery compatible protocol detects compatible classes only.""" diff --git a/tests/unit_tests/test_smart_scan.py b/tests/unit_tests/test_smart_scan.py index d8fe3038..b6e75085 100644 --- a/tests/unit_tests/test_smart_scan.py +++ b/tests/unit_tests/test_smart_scan.py @@ -281,8 +281,11 @@ def test_public_delete_scan(entered_smart_scan_thing, caplog): assert len(caplog.records) == 1 -def test_delete_all_scans(entered_smart_scan_thing, caplog): +def test_delete_all_scans(entered_smart_scan_thing, caplog, mocker): """Check the delete_all_scan API really does delete all the scans.""" + mock_raise_if_cancelled = mocker.patch( + "openflexure_microscope_server.things.smart_scan.lt.raise_if_cancelled", + ) smart_scan_thing = entered_smart_scan_thing with caplog.at_level(logging.INFO): fake_scan_names = [ @@ -295,12 +298,17 @@ def test_delete_all_scans(entered_smart_scan_thing, caplog): fake_scan_path = os.path.join(SCAN_DIR, fake_scan_name) os.makedirs(fake_scan_path) assert os.path.exists(fake_scan_path) - smart_scan_thing.delete_all_scans() + smart_scan_thing.delete_all_gallery_items() for fake_scan_name in fake_scan_names: fake_scan_path = os.path.join(SCAN_DIR, fake_scan_name) assert not os.path.exists(fake_scan_path) - # No logs generated - assert len(caplog.records) == 0 + # No One log generated per scan deleted + assert len(caplog.records) == 4 + for record in caplog.records: + assert record.message.startswith("Deleting: fake_scan_000") + assert record.levelname == "INFO" + # Check that raise_if_cancelled is called once for each scan. + assert mock_raise_if_cancelled.call_count == 4 def _run_only_outer_scan( diff --git a/webapp/src/components/tabContentComponents/galleryContent.vue b/webapp/src/components/tabContentComponents/galleryContent.vue index 42a17a42..45f8f2ac 100644 --- a/webapp/src/components/tabContentComponents/galleryContent.vue +++ b/webapp/src/components/tabContentComponents/galleryContent.vue @@ -19,14 +19,19 @@ @error="modalError" /> -