wip
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ScheduleItemStatus(str, Enum):
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
ABORTED = "aborted"
|
||||
@@ -1,25 +1,56 @@
|
||||
"""Small settings dialog for `notifications.NotificationSettings` - kept separate from the send logic itself."""
|
||||
"""
|
||||
Settings dialog for `notifications.NotificationSettings` - kept separate
|
||||
from the send logic itself (`notifications.py`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from qtpy.QtWidgets import QCheckBox, QDialog, QDialogButtonBox, QLabel, QVBoxLayout
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtWidgets import (
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QGroupBox,
|
||||
QLabel,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from .notifications import NotificationSettings
|
||||
from .enums import ScheduleItemStatus
|
||||
from .notifications import NotificationSettings, NotificationTarget, send_notification
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
def _service_state(client, name: str) -> tuple[bool, list[str]]:
|
||||
"""
|
||||
Best-effort read of a messaging service's enabled/scopes state (see
|
||||
the module docstring in notifications.py for why this is the only
|
||||
introspection available). Never raises - a client without `messaging`
|
||||
set up yet just looks like "not enabled, no scopes".
|
||||
"""
|
||||
messaging = getattr(client, "messaging", None)
|
||||
service = getattr(messaging, name, None) if messaging is not None else None
|
||||
if service is None:
|
||||
return False, []
|
||||
return bool(getattr(service, "_enabled", False)), sorted(getattr(service, "_scopes", set()))
|
||||
|
||||
|
||||
class NotificationSettingsDialog(QDialog):
|
||||
"""Choose whether, and for which item types, finish/fail notifications are sent."""
|
||||
"""Choose whether/where/for-which-item-types finish/fail notifications are sent."""
|
||||
|
||||
def __init__(self, settings: NotificationSettings, parent=None):
|
||||
def __init__(self, settings: NotificationSettings, client, parent=None):
|
||||
super().__init__(parent)
|
||||
self._client = client
|
||||
self.setWindowTitle("Notifications")
|
||||
self.setMinimumWidth(380)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
info_label = QLabel(
|
||||
"Sends a BEC notification (see MessageEndpoints.notification) whenever a "
|
||||
"schedule item finishes or fails. Whether it reaches you as a message depends "
|
||||
"on your facility's notification routing (SciHub + Signal/Teams/SciLog) being "
|
||||
"configured for these events - ask your beamline admin if you're not sure."
|
||||
"Sends a message through the selected BEC messaging service whenever a "
|
||||
"schedule item finishes or fails."
|
||||
)
|
||||
info_label.setWordWrap(True)
|
||||
layout.addWidget(info_label)
|
||||
@@ -28,32 +59,173 @@ class NotificationSettingsDialog(QDialog):
|
||||
self.enabled_check.setChecked(settings.enabled)
|
||||
layout.addWidget(self.enabled_check)
|
||||
|
||||
kind_box = QGroupBox("Notify for")
|
||||
kind_layout = QVBoxLayout(kind_box)
|
||||
self.scan_check = QCheckBox("Scans")
|
||||
self.scan_check.setChecked(settings.notify_scan)
|
||||
layout.addWidget(self.scan_check)
|
||||
|
||||
self.move_check = QCheckBox("Movements")
|
||||
self.move_check.setChecked(settings.notify_move)
|
||||
layout.addWidget(self.move_check)
|
||||
|
||||
self.rpc_check = QCheckBox("RPC / custom commands")
|
||||
self.rpc_check.setChecked(settings.notify_rpc)
|
||||
layout.addWidget(self.rpc_check)
|
||||
|
||||
for check in (self.scan_check, self.move_check, self.rpc_check):
|
||||
self.enabled_check.toggled.connect(check.setEnabled)
|
||||
check.setEnabled(settings.enabled)
|
||||
kind_layout.addWidget(check)
|
||||
layout.addWidget(kind_box)
|
||||
|
||||
target_box = QGroupBox("Send via")
|
||||
target_layout = QVBoxLayout(target_box)
|
||||
self.scilog_check = QCheckBox("SciLog")
|
||||
self.teams_check = QCheckBox("Microsoft Teams")
|
||||
self.signal_check = QCheckBox("Signal Messenger")
|
||||
for check in (self.scilog_check, self.teams_check, self.signal_check):
|
||||
target_layout.addWidget(check)
|
||||
layout.addWidget(target_box)
|
||||
|
||||
event_box = QGroupBox("Send on")
|
||||
event_layout = QVBoxLayout(event_box)
|
||||
self.completed_check = QCheckBox("Completed")
|
||||
self.aborted_check = QCheckBox("Aborted")
|
||||
self.failed_check = QCheckBox("Failed")
|
||||
for check in (self.completed_check, self.aborted_check, self.failed_check):
|
||||
event_layout.addWidget(check)
|
||||
layout.addWidget(event_box)
|
||||
|
||||
# Teams: contact/channel can only be a scope an admin has already
|
||||
# registered for the deployment - there's no way to address an
|
||||
# arbitrary Teams user from the client, so this is a dropdown, not
|
||||
# free text (see notifications.py's module docstring).
|
||||
self.teams_box = QGroupBox("Teams settings")
|
||||
teams_layout = QVBoxLayout(self.teams_box)
|
||||
teams_enabled, teams_scopes = _service_state(client, "teams")
|
||||
self.teams_combo = QComboBox()
|
||||
self.teams_combo.addItems(teams_scopes)
|
||||
self.teams_combo.setEnabled(teams_enabled)
|
||||
teams_layout.addWidget(QLabel("Contact or channel"))
|
||||
teams_layout.addWidget(self.teams_combo)
|
||||
if not teams_enabled:
|
||||
teams_layout.addWidget(_dim_label("Teams messaging is not enabled for this session."))
|
||||
elif not teams_scopes:
|
||||
teams_layout.addWidget(_dim_label("No Teams contacts/channels are registered."))
|
||||
layout.addWidget(self.teams_box)
|
||||
|
||||
# Signal: a raw phone number works directly (BEC normalizes it -
|
||||
# see the docs), so this is an editable combo box: type a number,
|
||||
# or pick a pre-registered scope if any exist.
|
||||
self.signal_box = QGroupBox("Signal settings")
|
||||
signal_layout = QVBoxLayout(self.signal_box)
|
||||
signal_enabled, signal_scopes = _service_state(client, "signal")
|
||||
self.signal_combo = QComboBox()
|
||||
self.signal_combo.setEditable(True)
|
||||
self.signal_combo.addItems(signal_scopes)
|
||||
self.signal_combo.setEnabled(signal_enabled)
|
||||
signal_layout.addWidget(QLabel("Phone number or contact"))
|
||||
self.signal_combo.setEditText("")
|
||||
self.signal_combo.lineEdit().setPlaceholderText("e.g. +41791234567")
|
||||
signal_layout.addWidget(self.signal_combo)
|
||||
if not signal_enabled:
|
||||
signal_layout.addWidget(_dim_label("Signal messaging is not enabled for this session."))
|
||||
layout.addWidget(self.signal_box)
|
||||
|
||||
# pre-fill from the persisted settings
|
||||
{
|
||||
NotificationTarget.SCILOG: self.scilog_check,
|
||||
NotificationTarget.TEAMS: self.teams_check,
|
||||
NotificationTarget.SIGNAL: self.signal_check,
|
||||
}.get(settings.target, self.scilog_check).setChecked(True)
|
||||
for event in settings.events:
|
||||
{
|
||||
ScheduleItemStatus.COMPLETED: self.completed_check,
|
||||
ScheduleItemStatus.ABORTED: self.aborted_check,
|
||||
ScheduleItemStatus.FAILED: self.failed_check,
|
||||
}.get(event).setChecked(True)
|
||||
if settings.teams_scope:
|
||||
idx = self.teams_combo.findText(settings.teams_scope)
|
||||
if idx >= 0:
|
||||
self.teams_combo.setCurrentIndex(idx)
|
||||
if settings.signal_number:
|
||||
self.signal_combo.setEditText(settings.signal_number)
|
||||
|
||||
self.scilog_check.toggled.connect(lambda *_: self._sync_target_widgets(self.scilog_check))
|
||||
self.teams_check.toggled.connect(lambda *_: self._sync_target_widgets(self.teams_check))
|
||||
self.signal_check.toggled.connect(lambda *_: self._sync_target_widgets(self.signal_check))
|
||||
|
||||
self.test_btn = QPushButton("Send test message")
|
||||
self.test_btn.clicked.connect(self._on_test_clicked)
|
||||
layout.addWidget(self.test_btn)
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
buttons.accepted.connect(self.accept)
|
||||
buttons.rejected.connect(self.reject)
|
||||
layout.addWidget(buttons)
|
||||
|
||||
def result_settings(self) -> NotificationSettings:
|
||||
"""Valid after `exec_()` returns `QDialog.Accepted`."""
|
||||
self._sync_target_widgets()
|
||||
|
||||
def _sync_target_widgets(self, current_button: QCheckBox = None):
|
||||
if current_button is None:
|
||||
for button in [self.scilog_check, self.teams_check, self.signal_check]:
|
||||
if button.isChecked():
|
||||
current_button = button
|
||||
break
|
||||
if current_button.isChecked():
|
||||
for button in [self.scilog_check, self.teams_check, self.signal_check]:
|
||||
if button != current_button:
|
||||
button.setChecked(False)
|
||||
if current_button == self.teams_check:
|
||||
self.teams_box.setVisible(True)
|
||||
self.signal_box.setVisible(False)
|
||||
elif current_button == self.signal_check:
|
||||
self.teams_box.setVisible(False)
|
||||
self.signal_box.setVisible(True)
|
||||
else:
|
||||
self.teams_box.setVisible(False)
|
||||
self.signal_box.setVisible(False)
|
||||
self.layout().activate()
|
||||
self.adjustSize()
|
||||
|
||||
def _current_settings(self) -> NotificationSettings:
|
||||
if self.scilog_check.isChecked():
|
||||
target = NotificationTarget.SCILOG
|
||||
elif self.teams_check.isChecked():
|
||||
target = NotificationTarget.TEAMS
|
||||
elif self.signal_check.isChecked():
|
||||
target = NotificationTarget.SIGNAL
|
||||
else:
|
||||
raise ValueError(f"Target {target} not supported")
|
||||
|
||||
events = []
|
||||
if self.completed_check.isChecked():
|
||||
events.append(ScheduleItemStatus.COMPLETED)
|
||||
if self.aborted_check.isChecked():
|
||||
events.append(ScheduleItemStatus.ABORTED)
|
||||
if self.failed_check.isChecked():
|
||||
events.append(ScheduleItemStatus.FAILED)
|
||||
|
||||
return NotificationSettings(
|
||||
enabled=self.enabled_check.isChecked(),
|
||||
notify_scan=self.scan_check.isChecked(),
|
||||
notify_move=self.move_check.isChecked(),
|
||||
notify_rpc=self.rpc_check.isChecked(),
|
||||
target=target,
|
||||
events=events,
|
||||
teams_scope=self.teams_combo.currentText().strip() or None,
|
||||
signal_number=self.signal_combo.currentText().strip() or None,
|
||||
)
|
||||
|
||||
def _on_test_clicked(self):
|
||||
settings = self._current_settings()
|
||||
try:
|
||||
send_notification(self._client, settings, "Test message from the BEC schedule widget.")
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
QMessageBox.critical(self, "Test message failed", str(exc))
|
||||
return
|
||||
QMessageBox.information(self, "Test message sent", "The test message was sent.")
|
||||
|
||||
def result_settings(self) -> NotificationSettings:
|
||||
"""Valid after `exec_()` returns `QDialog.Accepted`."""
|
||||
return self._current_settings()
|
||||
|
||||
|
||||
def _dim_label(text: str) -> QLabel:
|
||||
label = QLabel(text)
|
||||
label.setStyleSheet("color: gray;")
|
||||
label.setWordWrap(True)
|
||||
return label
|
||||
|
||||
@@ -2,42 +2,55 @@
|
||||
A small, self-contained "tell the user when something finished/failed"
|
||||
feature, independent of scheduling/execution logic.
|
||||
|
||||
Sends via BEC's own notification transport: `MessageEndpoints.notification`
|
||||
carries a `messages.NotificationMessage`, the same message type/endpoint
|
||||
BEC's own scan lifecycle events (e.g. "scan_completed") use. Reusing it
|
||||
means schedule-item notifications are just more entries in the same
|
||||
mechanism, not a plugin-invented side channel - and if your facility runs
|
||||
SciHub with messaging-service routing configured (Signal/Teams/SciLog),
|
||||
they can be forwarded out the same way any other BEC notification would
|
||||
be, by adding a route for the event names below in that routing config.
|
||||
Sends through BEC's own `client.messaging` container
|
||||
(`bec_lib.messaging_services.MessagingContainer`, exposed as `bec.messaging`
|
||||
in the IPython client) - the same mechanism documented at
|
||||
https://bec.readthedocs.io/latest/how-to/general/send-messages-to-signal.html
|
||||
and
|
||||
https://bec.readthedocs.io/latest/how-to/general/send-messages-to-scilog.html:
|
||||
|
||||
Nothing here is scheduling/BECWidget-specific: `notify_item_finished` only
|
||||
needs a connector, a settings object and a few plain values, so it can be
|
||||
called from `schedule_widget.py` without either module depending on the
|
||||
other's internals.
|
||||
bec.messaging.signal.new("Beamline checks completed.").send(scope="+41791234567")
|
||||
bec.messaging.scilog.new("Beamline checks completed.").send()
|
||||
bec.messaging.teams.new("Beamline checks completed.").send(scope=<registered scope>)
|
||||
|
||||
Notes on each service, read directly from `bec_lib.messaging_services`
|
||||
rather than assumed:
|
||||
|
||||
- **SciLog** needs no `scope` - it posts to the logbook of the currently
|
||||
active pgroup automatically.
|
||||
- **Signal** accepts a raw phone number as `scope` (BEC normalizes it,
|
||||
defaulting to the Swiss country code for numbers without one - see the
|
||||
docs above) *or* a pre-registered scope name for a group.
|
||||
- **Teams** is a plain `MessagingService` with no Teams-specific methods:
|
||||
a contact/channel can only be addressed by a `scope` an admin has
|
||||
already registered for the deployment (`service._scopes`) - there is no
|
||||
way to address an arbitrary Teams user/email directly from the client.
|
||||
This is why the Teams UI in `notification_dialog.py` is a dropdown of
|
||||
known scopes rather than free text, while Signal's is free text (with
|
||||
any registered scopes offered as suggestions).
|
||||
|
||||
`client.messaging.<service>._enabled` / `._scopes` are the only
|
||||
introspection available (no public accessor exists at the time of
|
||||
writing) - used here and in the dialog to grey out a target that isn't
|
||||
configured for the current session instead of only failing at send time.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from enum import Enum
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_lib.messages import MessagingServiceTextContent, NotificationMessage
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .enums import ScheduleItemStatus
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
# Namespaced so they can't collide with BEC's own built-in event names
|
||||
# (e.g. "scan_completed"); an admin who wants these routed to Signal/Teams/
|
||||
# SciLog can add a route for exactly these names in their notification
|
||||
# routing config (`MessageEndpoints.notification_config()`).
|
||||
_EVENT_NAMES = {
|
||||
("scan", True): "schedule_scan_completed",
|
||||
("scan", False): "schedule_scan_failed",
|
||||
("move", True): "schedule_move_completed",
|
||||
("move", False): "schedule_move_failed",
|
||||
("custom", True): "schedule_rpc_completed",
|
||||
("custom", False): "schedule_rpc_failed",
|
||||
}
|
||||
|
||||
class NotificationTarget(str, Enum):
|
||||
SCILOG = "scilog"
|
||||
TEAMS = "teams"
|
||||
SIGNAL = "signal"
|
||||
|
||||
|
||||
class NotificationSettings(BaseModel):
|
||||
@@ -51,6 +64,11 @@ class NotificationSettings(BaseModel):
|
||||
# any hand-typed command, so it's exposed to the user as "RPC".
|
||||
notify_rpc: bool = True
|
||||
|
||||
target: NotificationTarget = NotificationTarget.SCILOG
|
||||
events: list[ScheduleItemStatus] = []
|
||||
teams_scope: str | None = None # a pre-registered Teams scope (channel/contact)
|
||||
signal_number: str | None = None # a phone number, or a pre-registered Signal scope
|
||||
|
||||
|
||||
def _enabled_for_kind(settings: NotificationSettings, kind: str) -> bool:
|
||||
if not settings.enabled:
|
||||
@@ -62,33 +80,80 @@ def _enabled_for_kind(settings: NotificationSettings, kind: str) -> bool:
|
||||
}.get(kind, False)
|
||||
|
||||
|
||||
def send_notification(client, settings: NotificationSettings, text: str) -> None:
|
||||
"""
|
||||
Send `text` through whichever service `settings.target` selects.
|
||||
|
||||
Raises on failure (no target selected, missing Teams scope/Signal
|
||||
number, the target service not enabled for this session, ...) -
|
||||
callers that want a fire-and-forget send should catch around this.
|
||||
`notify_item_finished` does; the dialog's "Send test message" button
|
||||
deliberately does not, so the operator sees exactly what went wrong.
|
||||
"""
|
||||
if settings.target == NotificationTarget.SCILOG:
|
||||
client.messaging.scilog.new(text).send()
|
||||
elif settings.target == NotificationTarget.TEAMS:
|
||||
if not settings.teams_scope:
|
||||
raise ValueError("No Teams contact/channel selected.")
|
||||
client.messaging.teams.new(text).send(scope=settings.teams_scope)
|
||||
elif settings.target == NotificationTarget.SIGNAL:
|
||||
if not settings.signal_number:
|
||||
raise ValueError("No Signal phone number/contact set.")
|
||||
client.messaging.signal.new(text).send(scope=settings.signal_number)
|
||||
else:
|
||||
raise ValueError("No notification target selected.")
|
||||
|
||||
|
||||
def notify_item_finished(
|
||||
connector,
|
||||
schedule_name: str,
|
||||
client,
|
||||
kind: str,
|
||||
command: str,
|
||||
success: bool,
|
||||
scan_number: str,
|
||||
final_status: ScheduleItemStatus,
|
||||
settings: NotificationSettings,
|
||||
error: str | None = None,
|
||||
):
|
||||
"""
|
||||
Sends a `NotificationMessage` if notifications are enabled for `kind`.
|
||||
Never raises - a notification failing to send should not affect
|
||||
schedule execution.
|
||||
Sends a message if notifications are enabled for `kind` and a target
|
||||
is configured. Never raises - a notification failing to send should
|
||||
not affect schedule execution.
|
||||
"""
|
||||
if not _enabled_for_kind(settings, kind):
|
||||
return
|
||||
if final_status not in settings.events:
|
||||
return
|
||||
|
||||
event = _EVENT_NAMES.get((kind, success), "schedule_item_finished")
|
||||
status_word = "completed" if success else "failed"
|
||||
text = f"[{schedule_name}] {kind} {status_word}: {command}"
|
||||
status_word = final_status.name
|
||||
text = f"**BEC Scheduler Widget**\nStatus: {kind} {status_word}"
|
||||
if scan_number is not None:
|
||||
text = text + f"\nScan number: {scan_number}"
|
||||
text = text + f"\n\nCommand: {command}"
|
||||
if error:
|
||||
text += f"\n{error.strip().splitlines()[-1]}"
|
||||
|
||||
try:
|
||||
connector.send(
|
||||
MessageEndpoints.notification(event),
|
||||
NotificationMessage(event=event, message=[MessagingServiceTextContent(content=text)]),
|
||||
)
|
||||
send_notification(client, settings, text)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
logger.exception("Failed to send schedule notification for event '%s'", event)
|
||||
logger.exception(f"Failed to send schedule notification via {settings.target.value}")
|
||||
|
||||
|
||||
def notify_schedule_state(client, settings, schedule_name, state):
|
||||
"""
|
||||
Sends a message if notifications are enabled and a schedule is started or stopped.
|
||||
Never raises - a notification failing to send should not affect schedule execution.
|
||||
"""
|
||||
text = f'**BEC Scheduler Widget**\nSchedule "{schedule_name}" '
|
||||
if state == "started":
|
||||
text = text + "has started"
|
||||
elif state == "aborted":
|
||||
text = text + "was aborted"
|
||||
elif state == "widget_closed":
|
||||
text = text + "has finished prematurely because widget was closed"
|
||||
elif state == "finished":
|
||||
text = text + "has finished"
|
||||
else:
|
||||
logger.warning(f"Unknown schedule state {state}")
|
||||
try:
|
||||
send_notification(client, settings, text)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
logger.exception(f"Failed to send schedule notification via {settings.target.value}")
|
||||
|
||||
@@ -15,18 +15,10 @@ from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .enums import ScheduleItemStatus
|
||||
from .guard import GuardSettings
|
||||
from .notifications import NotificationSettings
|
||||
|
||||
|
||||
class ScheduleItemStatus(str, Enum):
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
ABORTED = "aborted"
|
||||
|
||||
|
||||
# Which tab of the Add/Edit dialog built `command`, and therefore how to
|
||||
# re-open that dialog pre-filled with the same values for editing. "custom"
|
||||
# covers hand-typed text (including RPC calls to other widgets), which has
|
||||
|
||||
@@ -46,12 +46,18 @@ from qtpy.QtWidgets import (
|
||||
)
|
||||
|
||||
from .endpoints import schedule as schedule_endpoint
|
||||
from .enums import ScheduleItemStatus
|
||||
from .guard import SignalGuard
|
||||
from .guard_dialog import GuardSettingsDialog
|
||||
from .item_dialog import ScheduleItemDialog
|
||||
from .notification_dialog import NotificationSettingsDialog
|
||||
from .notifications import NotificationSettings, notify_item_finished
|
||||
from .schedule_item import Schedule, ScheduleItem, ScheduleItemStatus
|
||||
from .notifications import (
|
||||
NotificationSettings,
|
||||
NotificationTarget,
|
||||
notify_item_finished,
|
||||
notify_schedule_state,
|
||||
)
|
||||
from .schedule_item import Schedule, ScheduleItem
|
||||
from .schedule_logic import index_of, pick_next_runnable, protected_prefix_length
|
||||
|
||||
logger = bec_logger.logger
|
||||
@@ -323,7 +329,9 @@ class Scheduler(BECWidget, QWidget):
|
||||
guard_row.addStretch()
|
||||
notifications_row = QHBoxLayout()
|
||||
self.notifications_btn = MyButton("Notifications", "default")
|
||||
self.notification_status_label = QLabel()
|
||||
notifications_row.addWidget(self.notifications_btn)
|
||||
notifications_row.addWidget(self.notification_status_label)
|
||||
notifications_row.addStretch()
|
||||
settings_layout.addLayout(guard_row)
|
||||
settings_layout.addLayout(notifications_row)
|
||||
@@ -346,6 +354,9 @@ class Scheduler(BECWidget, QWidget):
|
||||
|
||||
layout.addWidget(file_group)
|
||||
|
||||
self._update_guard_label()
|
||||
self._update_notifications_label()
|
||||
|
||||
self.apply_theme()
|
||||
|
||||
def apply_theme(self, theme: Optional[Literal["dark", "light"]] = None):
|
||||
@@ -694,14 +705,39 @@ class Scheduler(BECWidget, QWidget):
|
||||
# ---- notifications / auto-pause settings ---- #
|
||||
@SafeSlot()
|
||||
def _on_notifications_clicked(self):
|
||||
dialog = NotificationSettingsDialog(self.schedule.notifications, parent=self)
|
||||
dialog = NotificationSettingsDialog(self.schedule.notifications, self.client, parent=self)
|
||||
if dialog.exec_() != QDialog.Accepted:
|
||||
return
|
||||
with self._lock:
|
||||
self.schedule.notifications = dialog.result_settings()
|
||||
self._update_notifications_label()
|
||||
self._persist_locked()
|
||||
self._refresh_ui()
|
||||
|
||||
def _update_notifications_label(self):
|
||||
if self.schedule.notifications.enabled:
|
||||
types = []
|
||||
if self.schedule.notifications.notify_scan:
|
||||
types.append("scan")
|
||||
if self.schedule.notifications.notify_move:
|
||||
types.append("move")
|
||||
if self.schedule.notifications.notify_rpc:
|
||||
types.append("rpc")
|
||||
match self.schedule.notifications.target:
|
||||
case NotificationTarget.SIGNAL:
|
||||
target = "Signal Messenger"
|
||||
case NotificationTarget.TEAMS:
|
||||
target = "Microsoft Teams"
|
||||
case NotificationTarget.SCILOG:
|
||||
target = "Scilog"
|
||||
events = []
|
||||
for event in self.schedule.notifications.events:
|
||||
events.append(event.value)
|
||||
text = f"Enabled: Notify through {target}, for item type {types}, and events {events}"
|
||||
else:
|
||||
text = "Disabled"
|
||||
self.notification_status_label.setText(text)
|
||||
|
||||
@SafeSlot()
|
||||
def _on_guard_clicked(self):
|
||||
device_names = sorted(self.dev.keys())
|
||||
@@ -732,6 +768,7 @@ class Scheduler(BECWidget, QWidget):
|
||||
except Exception: # pylint: disable=broad-except
|
||||
logger.exception("Failed to cancel the running scan for the auto-pause guard")
|
||||
self.schedule_changed.emit()
|
||||
self._update_guard_label()
|
||||
|
||||
@SafeSlot(float)
|
||||
def _on_guard_resumed(self, value: float):
|
||||
@@ -1114,7 +1151,9 @@ class Scheduler(BECWidget, QWidget):
|
||||
super().cleanup()
|
||||
|
||||
def _run_all(self, repeat_aborted_item):
|
||||
logger.info("_run_all was called")
|
||||
notify_schedule_state(
|
||||
self.client, self.schedule.notifications, self.schedule_name, "started"
|
||||
)
|
||||
namespace = {"scans": self.scans, "dev": self.dev}
|
||||
while not self._abort_requested and not self._closing.is_set():
|
||||
with self._lock:
|
||||
@@ -1126,7 +1165,18 @@ class Scheduler(BECWidget, QWidget):
|
||||
self._await_running_item(next_item)
|
||||
else:
|
||||
self._execute_item(next_item, namespace)
|
||||
|
||||
if self._abort_requested:
|
||||
notify_schedule_state(
|
||||
self.client, self.schedule.notifications, self.schedule_name, "aborted"
|
||||
)
|
||||
elif self._closing.is_set():
|
||||
notify_schedule_state(
|
||||
self.client, self.schedule.notifications, self.schedule_name, "widget_closed"
|
||||
)
|
||||
else:
|
||||
notify_schedule_state(
|
||||
self.client, self.schedule.notifications, self.schedule_name, "finished"
|
||||
)
|
||||
with self._lock:
|
||||
logger.info("in _run_all, set is_running to false")
|
||||
self.schedule.is_running = False
|
||||
@@ -1209,13 +1259,17 @@ class Scheduler(BECWidget, QWidget):
|
||||
else:
|
||||
final_status = None
|
||||
|
||||
if final_status in (ScheduleItemStatus.COMPLETED, ScheduleItemStatus.FAILED):
|
||||
if final_status in (
|
||||
ScheduleItemStatus.COMPLETED,
|
||||
ScheduleItemStatus.FAILED,
|
||||
ScheduleItemStatus.ABORTED,
|
||||
):
|
||||
notify_item_finished(
|
||||
self.connector,
|
||||
self.schedule_name,
|
||||
self.client,
|
||||
item.kind,
|
||||
item.command,
|
||||
success=(final_status == ScheduleItemStatus.COMPLETED),
|
||||
item.scan_number,
|
||||
final_status=final_status,
|
||||
settings=self.schedule.notifications,
|
||||
error=item.error,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user