Merge branch 'start-gallery-thing' into 'v3'
Start implementing GalleryThing See merge request openflexure/openflexure-microscope-server!576
This commit is contained in:
commit
906b8be82c
13 changed files with 321 additions and 104 deletions
|
|
@ -8,6 +8,7 @@
|
|||
},
|
||||
"stage": "openflexure_microscope_server.things.stage.sangaboard:SangaboardThing",
|
||||
"autofocus": "openflexure_microscope_server.things.autofocus:AutofocusThing",
|
||||
"gallery": "openflexure_microscope_server.things.gallery.GalleryThing",
|
||||
"camera_stage_mapping": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper",
|
||||
"system": "openflexure_microscope_server.things.system:OpenFlexureSystem",
|
||||
"illumination": "openflexure_microscope_server.things.illumination:SangaIllumination",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
"camera": "openflexure_microscope_server.things.camera.simulation:SimulatedCamera",
|
||||
"stage": "openflexure_microscope_server.things.stage.dummy:DummyStage",
|
||||
"autofocus": "openflexure_microscope_server.things.autofocus:AutofocusThing",
|
||||
"gallery": "openflexure_microscope_server.things.gallery.GalleryThing",
|
||||
"camera_stage_mapping": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper",
|
||||
"illumination": "openflexure_microscope_server.things.illumination.SimulatorIllumination",
|
||||
"system": "openflexure_microscope_server.things.system:OpenFlexureSystem",
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -335,14 +335,25 @@ class ScanDirectoryManager:
|
|||
return [f.name for f in os.scandir(self._base_scan_dir) if f.is_dir()]
|
||||
|
||||
@requires_lock
|
||||
def all_scans_info(self, ongoing: Optional[str] = None) -> list[ScanInfo]:
|
||||
"""Return a lists of ScanInfo objects for each scan."""
|
||||
def all_scans_info(
|
||||
self, *, ongoing: Optional[str] = None, include_ongoing: bool = True
|
||||
) -> list[ScanInfo]:
|
||||
"""Return a lists of ScanInfo objects for each scan.
|
||||
|
||||
:param ongoing: The name of the ongoing scan (or None if no scan is ongoing).
|
||||
:param include_ongoing: True (dfault) to include the scan info of the ongoing
|
||||
scan in the return. False to exclude it.
|
||||
|
||||
:return: A list of ScanInfo objects for each scan.
|
||||
"""
|
||||
all_info: list[ScanInfo] = []
|
||||
for scan_name in self.all_scans:
|
||||
# If the scan is ongoing send flag to skip reading the json data
|
||||
skip_json = scan_name == ongoing
|
||||
scan_is_ongoing = scan_name == ongoing
|
||||
if scan_is_ongoing and not include_ongoing:
|
||||
continue
|
||||
scan_dir = ScanDirectory(scan_name, self.base_dir)
|
||||
info = scan_dir.scan_info(skip_json=skip_json)
|
||||
# If the scan is ongoing send flag to skip reading the json data
|
||||
info = scan_dir.scan_info(skip_json=scan_is_ongoing)
|
||||
all_info.append(info)
|
||||
return all_info
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,13 @@ class OFMThing(lt.Thing):
|
|||
|
||||
_data_dir: Optional[str] = None
|
||||
|
||||
_show_data_in_gallery: bool = False
|
||||
|
||||
@property
|
||||
def show_data_in_gallery(self) -> bool:
|
||||
"""Whether to show in the Gallery."""
|
||||
return self._show_data_in_gallery
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
"""Set the data directory when the Thing is entered."""
|
||||
# Note that the `application_config` was already validated when the
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ from pydantic import BaseModel, Field
|
|||
import labthings_fastapi as lt
|
||||
from labthings_fastapi.types.numpy import NDArray
|
||||
|
||||
from openflexure_microscope_server.things import OFMThing
|
||||
from openflexure_microscope_server.things.background_detect import (
|
||||
BackgroundDetectAlgorithm,
|
||||
)
|
||||
|
|
@ -164,7 +165,7 @@ class CameraMemoryBuffer:
|
|||
del self._storage[key]
|
||||
|
||||
|
||||
class BaseCamera(lt.Thing):
|
||||
class BaseCamera(OFMThing):
|
||||
"""The base class for all cameras. All cameras must directly inherit from this class.
|
||||
|
||||
The connection to the camera hardware should be added to the ``__enter__`` method not
|
||||
|
|
|
|||
99
src/openflexure_microscope_server/things/gallery.py
Normal file
99
src/openflexure_microscope_server/things/gallery.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
"""Server-side gallery functionality.
|
||||
|
||||
The types of data that are captured and how they display in the gallery are defined by
|
||||
the Things that capture the Data. This thing just provides a unified way for the gallery
|
||||
front end to access the data.
|
||||
"""
|
||||
|
||||
from typing import Any, Mapping, Optional, Protocol, Self, cast, runtime_checkable
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
import labthings_fastapi as lt
|
||||
|
||||
from openflexure_microscope_server.things import OFMThing
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class GalleryCompatibleThing(Protocol):
|
||||
"""Protocol for checking Things using the gallery are complete.
|
||||
|
||||
Note that runtime checkable protocols only check that the methods exist, not the
|
||||
full signatures. This means that anything attempting to define the methods will
|
||||
be picked up, but may throw an error when used.
|
||||
"""
|
||||
|
||||
# Ensure it is a Thing:
|
||||
_thing_server_interface: lt.ThingServerInterface
|
||||
|
||||
# Ensure it is an OFMThing:
|
||||
show_data_in_gallery: bool
|
||||
|
||||
gallery_data_name: str
|
||||
|
||||
gallery_data_schema: type[BaseModel]
|
||||
|
||||
# Ignore D102: No docstrings for the protocol.
|
||||
def get_data_for_gallery(self) -> list[BaseModel]: ... # noqa: D102
|
||||
|
||||
|
||||
class GalleryThing(lt.Thing):
|
||||
"""A Thing for communicating with the front end gallery."""
|
||||
|
||||
all_ofm_things: Mapping[str, OFMThing] = lt.thing_slot()
|
||||
|
||||
_gallery_providing_things: Optional[Mapping[str, GalleryCompatibleThing]] = None
|
||||
|
||||
@property
|
||||
def gallery_providing_things(self) -> Mapping[str, GalleryCompatibleThing]:
|
||||
"""All Things that provide data to the gallery."""
|
||||
if self._gallery_providing_things is None:
|
||||
raise RuntimeError(
|
||||
"Cannot access gallery_providing_things before server has started."
|
||||
)
|
||||
return self._gallery_providing_things
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
"""Check for all gallery providing things on server startup."""
|
||||
self._set_gallery_providers()
|
||||
return self
|
||||
|
||||
def _set_gallery_providers(self) -> None:
|
||||
"""Set the mapping of gallery providing things.
|
||||
|
||||
This is called on ``__enter__`` when the server starts.
|
||||
"""
|
||||
gallery_providers = {
|
||||
name: thing
|
||||
for name, thing in self.all_ofm_things.items()
|
||||
if thing.show_data_in_gallery
|
||||
}
|
||||
# cache initial list of keys as it may change in the loop
|
||||
keys = list(gallery_providers.keys())
|
||||
for key in keys:
|
||||
if not isinstance(gallery_providers[key], GalleryCompatibleThing):
|
||||
self.logger.error(
|
||||
f"Data from {key} cannot be shown in gallery as it does not "
|
||||
"provide all necessary properties/methods for the gallery."
|
||||
)
|
||||
gallery_providers.pop(key)
|
||||
|
||||
# Cast the type of each thing to "GalleryCompatibleThing" as other Things have
|
||||
# been popped.
|
||||
self._gallery_providing_things = cast(
|
||||
Mapping[str, GalleryCompatibleThing], gallery_providers
|
||||
)
|
||||
|
||||
@lt.property
|
||||
def list_data(self) -> list[dict[str, Any]]:
|
||||
"""List the data from all registered things.
|
||||
|
||||
Currently only works with `smart_scan` as the UI cards are not customisable.
|
||||
|
||||
This will change to an action (or another type of endpoint) at a
|
||||
later date to enable filtering, and returning only a specific page.
|
||||
"""
|
||||
data_list = []
|
||||
for thing in self.gallery_providing_things.values():
|
||||
data_list += [model.model_dump() for model in thing.get_data_for_gallery()]
|
||||
return data_list
|
||||
|
|
@ -71,16 +71,6 @@ class ActiveScanData(scan_directories.BaseScanData):
|
|||
self.scan_result = result
|
||||
|
||||
|
||||
class ScanListInfo(BaseModel):
|
||||
"""The information to be sent to the Scan List tab."""
|
||||
|
||||
scans: list[scan_directories.ScanInfo]
|
||||
"""The list of scans as ScanInfo objects"""
|
||||
|
||||
ongoing: Optional[str]
|
||||
"""The name of the ongoing scan or None"""
|
||||
|
||||
|
||||
class JPEGBlob(lt.blob.Blob):
|
||||
"""A class representing a JPEG image as a LabThings FastAPI Blob."""
|
||||
|
||||
|
|
@ -176,6 +166,31 @@ class SmartScanThing(OFMThing):
|
|||
In this case it doesn't need to do anything.
|
||||
"""
|
||||
|
||||
# Register with gallery.
|
||||
_show_data_in_gallery = True
|
||||
|
||||
@property
|
||||
def gallery_data_name(self) -> str:
|
||||
"""Name under which data shows up in gallery."""
|
||||
return "Scans"
|
||||
|
||||
@property
|
||||
def gallery_data_schema(self) -> type[scan_directories.ScanInfo]:
|
||||
"""The schema (BaseModel) for passing data to the gallery."""
|
||||
return scan_directories.ScanInfo
|
||||
|
||||
def get_data_for_gallery(self) -> list[scan_directories.ScanInfo]:
|
||||
"""Return all the information from the scan directories.
|
||||
|
||||
It is preferable to use the method rather than calling
|
||||
_scan_dir_manager.all_scans_info() directly as it will handle stopping the json
|
||||
in any ongoing scans being read.
|
||||
"""
|
||||
ongoing_name = None if self._ongoing_scan is None else self.ongoing_scan.name
|
||||
return self._scan_dir_manager.all_scans_info(
|
||||
ongoing=ongoing_name, include_ongoing=False
|
||||
)
|
||||
|
||||
# Note that the default detector name is set at init. This is over written if
|
||||
# setting is loaded from disk.
|
||||
@lt.setting
|
||||
|
|
@ -544,31 +559,6 @@ class SmartScanThing(OFMThing):
|
|||
stitch_automatically: bool = lt.setting(default=True)
|
||||
"""Whether to run a final stitch at the end of a successful scan."""
|
||||
|
||||
def _get_all_scan_info(self) -> list[scan_directories.ScanInfo]:
|
||||
"""Return all the information from the scan directories.
|
||||
|
||||
It is preferable to use the method rather than calling
|
||||
_scan_dir_manager.all_scans_info() directly as it will handle stopping the json
|
||||
in any ongoing scans being read.
|
||||
"""
|
||||
ongoing_name = None if self._ongoing_scan is None else self.ongoing_scan.name
|
||||
return self._scan_dir_manager.all_scans_info(ongoing=ongoing_name)
|
||||
|
||||
@lt.property
|
||||
def scans(self) -> ScanListInfo:
|
||||
"""All the available scans.
|
||||
|
||||
Each scan has a name (which can be used to access it), along with
|
||||
its modified and created times (according to the filesystem) and
|
||||
the number of items in the ``images`` folder. Note that image count
|
||||
uses a regular expression, and changes to the naming scheme will
|
||||
break it.
|
||||
"""
|
||||
return ScanListInfo(
|
||||
scans=self._get_all_scan_info(),
|
||||
ongoing=None if self._ongoing_scan is None else self.ongoing_scan.name,
|
||||
)
|
||||
|
||||
@lt.endpoint(
|
||||
"get",
|
||||
"get_stitch/{scan_name}",
|
||||
|
|
@ -632,8 +622,8 @@ class SmartScanThing(OFMThing):
|
|||
@lt.action
|
||||
def purge_empty_scans(self) -> None:
|
||||
"""Delete all scan folders containing no images at the top level."""
|
||||
# JSON is ignored as it's created before any images are captured
|
||||
for scan_info in self._get_all_scan_info():
|
||||
# Use the scan list (the data read by the gallery) to check for empty scans.
|
||||
for scan_info in self.get_data_for_gallery():
|
||||
if scan_info.number_of_images == 0:
|
||||
self._delete_scan(scan_info.name)
|
||||
|
||||
|
|
@ -748,6 +738,8 @@ class SmartScanThing(OFMThing):
|
|||
"""
|
||||
if self._scan_lock.locked():
|
||||
raise RuntimeError("Can't stitch previous scans while a scan is ongoing")
|
||||
for scan in self._get_all_scan_info():
|
||||
# Use the scan list (the data read by the gallery) to find any scans that
|
||||
# need stitching.
|
||||
for scan in self.get_data_for_gallery():
|
||||
if scan.dzi is None:
|
||||
self.stitch_scan(scan_name=scan.name)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
"""Fixtures for all test suites."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from collections.abc import Iterable
|
||||
from contextlib import contextmanager
|
||||
|
|
@ -10,6 +12,24 @@ import pytest
|
|||
|
||||
from labthings_fastapi.testing import create_thing_without_server
|
||||
|
||||
from .shared_utils.lt_test_utils import LabThingsTestEnv
|
||||
|
||||
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
REPO_ROOT = os.path.dirname(THIS_DIR)
|
||||
SIM_CONFIG = os.path.join(REPO_ROOT, "ofm_config_simulation.json")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def simulation_test_env():
|
||||
"""Yield a server with the configuration from the simulation json."""
|
||||
with open(SIM_CONFIG, "r", encoding="utf-8") as f_obj:
|
||||
config_dict = json.load(f_obj)
|
||||
with LabThingsTestEnv(
|
||||
things=config_dict["things"],
|
||||
application_config=config_dict["application_config"],
|
||||
) as env:
|
||||
yield env
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def check_side_effect(caplog):
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ it has been moved as it artificaially inflated coverage.
|
|||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import piexif
|
||||
|
|
@ -21,29 +20,11 @@ from PIL import Image
|
|||
|
||||
import labthings_fastapi as lt
|
||||
|
||||
from ..shared_utils.lt_test_utils import LabThingsTestEnv
|
||||
|
||||
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
REPO_ROOT = os.path.dirname(os.path.dirname(THIS_DIR))
|
||||
SIM_CONFIG = os.path.join(REPO_ROOT, "ofm_config_simulation.json")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_env():
|
||||
"""Yield a server with a very basic configuration."""
|
||||
with open(SIM_CONFIG, "r", encoding="utf-8") as f_obj:
|
||||
config_dict = json.load(f_obj)
|
||||
with LabThingsTestEnv(
|
||||
things=config_dict["things"],
|
||||
application_config=config_dict["application_config"],
|
||||
) as env:
|
||||
yield env
|
||||
|
||||
|
||||
def test_autofocus(test_env):
|
||||
def test_autofocus(simulation_test_env):
|
||||
"""Test Fast Autofocus can run doesn't raise an exception."""
|
||||
stage = test_env.get_thing_client("stage")
|
||||
autofocus = test_env.get_thing_client("autofocus")
|
||||
stage = simulation_test_env.get_thing_client("stage")
|
||||
autofocus = simulation_test_env.get_thing_client("autofocus")
|
||||
assert stage.position["z"] == 0
|
||||
# Autofocus 5 times and check each ends within 500 steps
|
||||
for i in range(5):
|
||||
|
|
@ -51,17 +32,17 @@ def test_autofocus(test_env):
|
|||
assert abs(stage.position["z"]) < 500, f"Autofocus failed on iteration {i}"
|
||||
|
||||
|
||||
def test_grab_jpeg(test_env):
|
||||
def test_grab_jpeg(simulation_test_env):
|
||||
"""Check that grab_jpeg returns a blob that can be opened."""
|
||||
camera = test_env.get_thing_client("camera")
|
||||
camera = simulation_test_env.get_thing_client("camera")
|
||||
blob = camera.grab_jpeg()
|
||||
image = Image.open(blob.open())
|
||||
assert image.size == (820, 616)
|
||||
|
||||
|
||||
def test_capture_jpeg_metadata(test_env):
|
||||
def test_capture_jpeg_metadata(simulation_test_env):
|
||||
"""Check that the position is encoded into the image metadata."""
|
||||
camera = test_env.get_thing_client("camera")
|
||||
camera = simulation_test_env.get_thing_client("camera")
|
||||
blob = camera.capture_jpeg()
|
||||
image = Image.open(blob.open())
|
||||
exif_dict = piexif.load(image.info["exif"])
|
||||
|
|
@ -72,9 +53,9 @@ def test_capture_jpeg_metadata(test_env):
|
|||
assert image.size == (820, 616)
|
||||
|
||||
|
||||
def test_stage(test_env):
|
||||
def test_stage(simulation_test_env):
|
||||
"""Test moving the stage forwards and backwards."""
|
||||
stage = test_env.get_thing_client("stage")
|
||||
stage = simulation_test_env.get_thing_client("stage")
|
||||
start = stage.position
|
||||
move = {"x": 1, "y": 2, "z": 3}
|
||||
move_back = {"x": -1, "y": -2, "z": -3}
|
||||
|
|
@ -95,20 +76,20 @@ def test_stage(test_env):
|
|||
assert start["z"] == pos["z"]
|
||||
|
||||
|
||||
def test_capture_array(test_env):
|
||||
def test_capture_array(simulation_test_env):
|
||||
"""Capture array from simulation and check the size is as expected."""
|
||||
camera = test_env.get_thing_client("camera")
|
||||
camera = simulation_test_env.get_thing_client("camera")
|
||||
array = np.asarray(camera.capture_array())
|
||||
assert array.shape == (616, 820, 3)
|
||||
|
||||
|
||||
def test_camera_stage_mapping_calibration(test_env):
|
||||
def test_camera_stage_mapping_calibration(simulation_test_env):
|
||||
"""Check that camera stage mapping runs and returns the expected result."""
|
||||
camera = test_env.get_thing_client("camera")
|
||||
camera = simulation_test_env.get_thing_client("camera")
|
||||
# Remove camera settling time for speed.
|
||||
camera.settling_time = 0
|
||||
csm = test_env.get_thing_client("camera_stage_mapping")
|
||||
stage = test_env.get_thing_client("stage")
|
||||
csm = simulation_test_env.get_thing_client("camera_stage_mapping")
|
||||
stage = simulation_test_env.get_thing_client("stage")
|
||||
|
||||
# Check it starts uncalibrated
|
||||
assert csm.calibration_required
|
||||
|
|
|
|||
122
tests/unit_tests/test_gallery.py
Normal file
122
tests/unit_tests/test_gallery.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
"""Tests that captures have the expected metadata."""
|
||||
|
||||
import logging
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from labthings_fastapi.testing import create_thing_without_server
|
||||
|
||||
from openflexure_microscope_server.things import OFMThing
|
||||
from openflexure_microscope_server.things.gallery import (
|
||||
GalleryCompatibleThing,
|
||||
GalleryThing,
|
||||
)
|
||||
|
||||
from ..shared_utils.lt_test_utils import LabThingsTestEnv
|
||||
|
||||
|
||||
class MockGalleryData(BaseModel):
|
||||
"""Mock gallery data."""
|
||||
|
||||
mock: str
|
||||
foobar: int
|
||||
|
||||
|
||||
class MinimalGalleryClass:
|
||||
"""A class that provides data to the gallery but is NOT a thing.
|
||||
|
||||
This should fail the protocol check as it is not an OFMThing
|
||||
"""
|
||||
|
||||
gallery_data_name: str = "Mock"
|
||||
|
||||
gallery_data_schema: type[BaseModel] = MockGalleryData
|
||||
|
||||
def get_data_for_gallery(self) -> list[BaseModel]:
|
||||
"""Return a list of Mock Gallery Data."""
|
||||
return [MockGalleryData(mock="mock", foobar="foobar")]
|
||||
|
||||
|
||||
class MinimalGallerySource(OFMThing, MinimalGalleryClass):
|
||||
"""Minimal example of a Thing that can show data in the gallery.
|
||||
|
||||
Mixes MinimalGalleryClass into OFMThing to be recognised by the protocol.
|
||||
"""
|
||||
|
||||
show_data_in_gallery = True
|
||||
|
||||
|
||||
class BadGallerySource(OFMThing):
|
||||
"""An OFMThing that almost defines enough to show in the gallery.
|
||||
|
||||
Shouldn't match the protocol as ``gallery_data_name`` is missing.
|
||||
"""
|
||||
|
||||
show_data_in_gallery = True
|
||||
|
||||
gallery_data_schema: type[BaseModel] = MockGalleryData
|
||||
|
||||
def get_data_for_gallery(self) -> list[BaseModel]:
|
||||
"""Return a list of Mock Gallery Data."""
|
||||
return [MockGalleryData(mock="mock", foobar="foobar")]
|
||||
|
||||
|
||||
def test_gallery_compatible_protocol():
|
||||
"""Check the gallery compatible protocol detects compatible classes only."""
|
||||
# Minimal class has the gallery specific methods but is not an OFMThing.
|
||||
assert not isinstance(MinimalGalleryClass(), GalleryCompatibleThing)
|
||||
|
||||
# BadGallerySource is an OFM Thing but is missing one of the required properties.
|
||||
assert not isinstance(
|
||||
create_thing_without_server(BadGallerySource), GalleryCompatibleThing
|
||||
)
|
||||
|
||||
# MinimalGallerySource should match!
|
||||
assert isinstance(
|
||||
create_thing_without_server(MinimalGallerySource), GalleryCompatibleThing
|
||||
)
|
||||
|
||||
|
||||
def test_gallery_thing_finds_all_providers(simulation_test_env):
|
||||
"""Check the gallery identifies the correct Things that will provide data."""
|
||||
gallery = simulation_test_env.get_thing_by_name("gallery")
|
||||
snake_workflow = simulation_test_env.get_thing_by_name("snake_workflow")
|
||||
smart_scan = simulation_test_env.get_thing_by_name("smart_scan")
|
||||
camera = simulation_test_env.get_thing_by_name("camera")
|
||||
|
||||
assert snake_workflow not in gallery.all_ofm_things.values()
|
||||
assert camera in gallery.all_ofm_things.values()
|
||||
assert smart_scan in gallery.all_ofm_things.values()
|
||||
|
||||
assert snake_workflow not in gallery.gallery_providing_things.values()
|
||||
assert camera not in gallery.gallery_providing_things.values()
|
||||
assert smart_scan in gallery.gallery_providing_things.values()
|
||||
|
||||
assert not isinstance(snake_workflow, GalleryCompatibleThing)
|
||||
assert isinstance(smart_scan, GalleryCompatibleThing)
|
||||
|
||||
|
||||
def test_gallery_logs_for_bad_providers(caplog):
|
||||
"""Test that an error is logged if a gallery source doesn't match the protocol."""
|
||||
# Create a config with the minimal Thing and the bad Thing
|
||||
things = {
|
||||
"minimal_thing": MinimalGallerySource,
|
||||
"bad_thing": BadGallerySource,
|
||||
"gallery": GalleryThing,
|
||||
}
|
||||
with caplog.at_level(logging.INFO):
|
||||
# Start the server
|
||||
with LabThingsTestEnv(
|
||||
things=things, application_config={"data_folder": "./openflexure/data/"}
|
||||
) as env:
|
||||
gallery = env.get_thing_by_name("gallery")
|
||||
# Minimal thing is listed as a gallery provider
|
||||
assert "minimal_thing" in gallery.gallery_providing_things
|
||||
# Bad thing isn't as the protocol doesn't match
|
||||
assert "bad_thing" not in gallery.gallery_providing_things
|
||||
|
||||
# There is an error logged about bad_thing
|
||||
assert len(caplog.records) == 1
|
||||
record = caplog.records[0]
|
||||
assert "Data from bad_thing cannot be shown in gallery" in record.message
|
||||
assert record.levelname == "ERROR"
|
||||
|
|
@ -13,8 +13,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<h3 class="uk-card-title scan-card-title">{{ scanData.name }}</h3>
|
||||
<h4 v-if="ongoing" class="ongoing-msg">Scan in progress</h4>
|
||||
<div v-if="!ongoing" class="button-container">
|
||||
<div class="button-container">
|
||||
<div class="uk-button-group scan-card-buttons">
|
||||
<action-button
|
||||
class="uk-width-1-2"
|
||||
|
|
@ -59,10 +58,9 @@
|
|||
<ul>
|
||||
<li>{{ scanData.number_of_images }} images</li>
|
||||
<li>Created: {{ formatDate(scanData.created) }}</li>
|
||||
<li v-if="!ongoing">Duration: {{ formatDuration(scanData.duration) }}</li>
|
||||
<li v-if="ongoing">Duration: <i>Ongoing</i></li>
|
||||
<li>Duration: {{ formatDuration(scanData.duration) }}</li>
|
||||
</ul>
|
||||
<ul v-if="!ongoing">
|
||||
<ul>
|
||||
<li v-if="scanData.number_of_images < 3" class="warning-msg">
|
||||
Not enough images to stitch
|
||||
</li>
|
||||
|
|
@ -102,10 +100,6 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
ongoing: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
emits: ["viewer-requested", "update-requested"],
|
||||
|
|
@ -148,9 +142,7 @@ export default {
|
|||
},
|
||||
requestViewer() {
|
||||
// Notify parent that thumbnail was clicked
|
||||
if (!this.ongoing) {
|
||||
this.$emit("viewer-requested", this.scanData);
|
||||
}
|
||||
this.$emit("viewer-requested", this.scanData);
|
||||
},
|
||||
async deleteScan() {
|
||||
try {
|
||||
|
|
@ -204,9 +196,4 @@ ul {
|
|||
.scan-card-title {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ongoing-msg {
|
||||
text-align: center;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@
|
|||
<scan-card
|
||||
:scan-data="scanData"
|
||||
:scans-uri="scansUri"
|
||||
:ongoing="isOngoing(scanData.name)"
|
||||
@viewer-requested="showScan"
|
||||
@update-requested="updateScans"
|
||||
/>
|
||||
|
|
@ -97,7 +96,6 @@ export default {
|
|||
data: function () {
|
||||
return {
|
||||
scans: [],
|
||||
ongoing: null,
|
||||
selectedScan: null,
|
||||
osdViewer: null,
|
||||
currentPage: 1,
|
||||
|
|
@ -108,7 +106,9 @@ export default {
|
|||
computed: {
|
||||
...mapState(useSettingsStore, ["baseUri", "ready"]),
|
||||
scansUri() {
|
||||
return this.thingPropertyUrl("smart_scan", "scans");
|
||||
// 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);
|
||||
},
|
||||
scansEmpty() {
|
||||
return this.scans.length == 0;
|
||||
|
|
@ -179,9 +179,7 @@ export default {
|
|||
},
|
||||
async updateScans() {
|
||||
try {
|
||||
let scans_information = await this.readThingProperty("smart_scan", "scans");
|
||||
let scans = scans_information.scans;
|
||||
this.ongoing = scans_information.ongoing;
|
||||
let scans = await this.readThingProperty("gallery", "list_data");
|
||||
if (!scans | (scans.length == 0)) {
|
||||
this.scans = scans;
|
||||
}
|
||||
|
|
@ -198,9 +196,6 @@ export default {
|
|||
this.scans = [];
|
||||
}
|
||||
},
|
||||
isOngoing(name) {
|
||||
return name === this.ongoing;
|
||||
},
|
||||
async deleteAllScans() {
|
||||
try {
|
||||
await this.modalConfirm(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue