Gallery can delete all data.

This commit is contained in:
Julian Stirling 2026-06-21 18:24:57 +01:00
parent bef63769df
commit cc6caa39d7
6 changed files with 85 additions and 58 deletions

View file

@ -298,8 +298,8 @@ class BaseCamera(OFMThing, ABC):
"""The schema (BaseModel) for passing data to the gallery.""" """The schema (BaseModel) for passing data to the gallery."""
return CaptureInfo return CaptureInfo
def get_data_for_gallery(self) -> list[CaptureInfo]: def _all_captures(self) -> list[str]:
"""Return all the information about the saved captures.""" """Return the full path for all captures on disk."""
files = os.listdir(self._data_dir) files = os.listdir(self._data_dir)
captures = [] captures = []
for filename in files: for filename in files:
@ -308,15 +308,30 @@ class BaseCamera(OFMThing, ABC):
BASE_IMAGE_FORMATS["jpeg"].path_matches(filename) BASE_IMAGE_FORMATS["jpeg"].path_matches(filename)
or BASE_IMAGE_FORMATS["png"].path_matches(filename) or BASE_IMAGE_FORMATS["png"].path_matches(filename)
): ):
captures.append( captures.append(full_path)
return captures
def get_data_for_gallery(self) -> list[CaptureInfo]:
"""Return all the information about the saved captures."""
return [
CaptureInfo( CaptureInfo(
name=filename, name=os.path.basename(capture),
created=os.path.getctime(full_path), created=os.path.getctime(capture),
modified=os.path.getmtime(full_path), modified=os.path.getmtime(capture),
thing=self.name, thing=self.name,
) )
) for capture in self._all_captures()
return 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( @lt.endpoint(
"delete", "delete",

View file

@ -36,6 +36,8 @@ class GalleryCompatibleThing(Protocol):
# Ignore D102: No docstrings for the protocol. # Ignore D102: No docstrings for the protocol.
def get_data_for_gallery(self) -> list[BaseModel]: ... # noqa: D102 def get_data_for_gallery(self) -> list[BaseModel]: ... # noqa: D102
def delete_all_gallery_items(self) -> None: ... # noqa: D102
class GalleryThing(lt.Thing): class GalleryThing(lt.Thing):
"""A Thing for communicating with the front end gallery.""" """A Thing for communicating with the front end gallery."""
@ -95,5 +97,18 @@ class GalleryThing(lt.Thing):
""" """
data_list = [] data_list = []
for thing in self.gallery_providing_things.values(): 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 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()

View file

@ -192,6 +192,18 @@ class SmartScanThing(OFMThing):
ongoing=ongoing_name, include_ongoing=False 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 # Note that the default detector name is set at init. This is over written if
# setting is loaded from disk. # setting is loaded from disk.
@lt.setting @lt.setting
@ -608,20 +620,6 @@ class SmartScanThing(OFMThing):
if not deleted_scan_success: if not deleted_scan_success:
raise HTTPException(400, "Couldn't delete scan, check log for details") 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 @lt.action
def purge_empty_scans(self) -> None: def purge_empty_scans(self) -> None:
"""Delete all scan folders containing no images at the top level.""" """Delete all scan folders containing no images at the top level."""

View file

@ -36,6 +36,9 @@ class MinimalGalleryClass:
"""Return a list of Mock Gallery Data.""" """Return a list of Mock Gallery Data."""
return [MockGalleryData(mock="mock", foobar="foobar")] return [MockGalleryData(mock="mock", foobar="foobar")]
def delete_all_gallery_items(self) -> None:
"""Mock deleting all the data."""
class MinimalGallerySource(OFMThing, MinimalGalleryClass): class MinimalGallerySource(OFMThing, MinimalGalleryClass):
"""Minimal example of a Thing that can show data in the gallery. """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 a list of Mock Gallery Data."""
return [MockGalleryData(mock="mock", foobar="foobar")] return [MockGalleryData(mock="mock", foobar="foobar")]
def delete_all_gallery_items(self) -> None:
"""Mock deleting all the data."""
def test_gallery_compatible_protocol(): def test_gallery_compatible_protocol():
"""Check the gallery compatible protocol detects compatible classes only.""" """Check the gallery compatible protocol detects compatible classes only."""

View file

@ -281,8 +281,11 @@ def test_public_delete_scan(entered_smart_scan_thing, caplog):
assert len(caplog.records) == 1 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.""" """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 smart_scan_thing = entered_smart_scan_thing
with caplog.at_level(logging.INFO): with caplog.at_level(logging.INFO):
fake_scan_names = [ 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) fake_scan_path = os.path.join(SCAN_DIR, fake_scan_name)
os.makedirs(fake_scan_path) os.makedirs(fake_scan_path)
assert os.path.exists(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: for fake_scan_name in fake_scan_names:
fake_scan_path = os.path.join(SCAN_DIR, fake_scan_name) fake_scan_path = os.path.join(SCAN_DIR, fake_scan_name)
assert not os.path.exists(fake_scan_path) assert not os.path.exists(fake_scan_path)
# No logs generated # No One log generated per scan deleted
assert len(caplog.records) == 0 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( def _run_only_outer_scan(

View file

@ -19,14 +19,19 @@
@error="modalError" @error="modalError"
/> />
</div> </div>
<div> <div class="gallery-button">
<button <action-button
class="uk-button uk-button-default uk-width-1-1 gallery-button" class="uk-width-1-1"
type="button" thing="gallery"
@click="deleteAllScans()" action="delete_all_data"
> submit-label="Delete All"
Delete All Scans :can-terminate="true"
</button> :button-primary="false"
:modal-progress="true"
:requires-confirmation="true"
:confirmation-message="'<p>Are you sure you want to delete all gallery data from the microscope?</p><p>This is <b>irreversible</b>!</p>'"
@error="modalError"
/>
</div> </div>
<div> <div>
<button <button
@ -69,11 +74,10 @@
</template> </template>
<script> <script>
import axios from "axios";
import PaginateLinks from "@/components/genericComponents/paginateLinks.vue"; import PaginateLinks from "@/components/genericComponents/paginateLinks.vue";
import actionButton from "../labThingsComponents/actionButton.vue"; import actionButton from "../labThingsComponents/actionButton.vue";
import galleryCard from "./galleryComponents/galleryCard.vue"; import galleryCard from "./galleryComponents/galleryCard.vue";
import galleryModal from "./galleryComponents/galleryViewer.vue/index.js"; import galleryModal from "./galleryComponents/galleryViewer.vue";
import { eventBus } from "../../eventBus.js"; import { eventBus } from "../../eventBus.js";
import { useIntersectionObserver } from "@vueuse/core"; import { useIntersectionObserver } from "@vueuse/core";
import { useSettingsStore } from "@/stores/settings.js"; import { useSettingsStore } from "@/stores/settings.js";
@ -104,11 +108,6 @@ export default {
computed: { computed: {
...mapState(useSettingsStore, ["baseUri", "ready"]), ...mapState(useSettingsStore, ["baseUri", "ready"]),
scansUri() {
// The scans URI is currently used for creating endpoint URIs.
// The actual property does not exist. So allowUndefined=true
return this.thingPropertyUrl("smart_scan", "scans", true);
},
noItems() { noItems() {
return !this.all_items || this.all_items?.length === 0; return !this.all_items || this.all_items?.length === 0;
}, },
@ -188,20 +187,6 @@ export default {
this.all_items = []; this.all_items = [];
} }
}, },
async deleteAllScans() {
try {
await this.modalConfirm(
"Are you sure you want to delete all scans from the microscope? " +
"This is <b>irreversible</b>!",
);
await axios.delete(`${this.scansUri}`);
await this.refreshGallery();
this.modalNotify("Deleted all scans.");
} catch (e) {
// if the confirmation was cancelled, it's rejected with null error
if (e) this.modalError(e);
}
},
showItem(itemData) { showItem(itemData) {
if (itemData.card_type === "Scan") { if (itemData.card_type === "Scan") {
if (itemData.dzi) { if (itemData.dzi) {