From 167a2f23dc0efcd4389d3a1e9f2b1447e2848d00 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Mon, 26 Jan 2026 19:51:33 +0000 Subject: [PATCH 01/43] Cleanup server side code and comments --- server/digi_server/settings.py | 16 ---------------- server/utils/web/base_controller.py | 2 -- 2 files changed, 18 deletions(-) diff --git a/server/digi_server/settings.py b/server/digi_server/settings.py index 960adde6..c2b8cf57 100644 --- a/server/digi_server/settings.py +++ b/server/digi_server/settings.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import json import os import tomllib @@ -17,12 +15,6 @@ def _get_version() -> str: - """ - Read version from pyproject.toml. - - Returns: - str: Version string from pyproject.toml, or '0.0.0' if not found - """ try: # Get path to pyproject.toml (one directory up from digi_server) pyproject_path = Path(__file__).parent.parent / "pyproject.toml" @@ -37,14 +29,6 @@ def _get_version() -> str: def get_version() -> str: - """ - Get the current application version. - - Public wrapper for _get_version() that can be imported by other modules. - - Returns: - str: Version string from pyproject.toml, or '0.0.0' if not found - """ return _get_version() diff --git a/server/utils/web/base_controller.py b/server/utils/web/base_controller.py index 5aa3b422..17f099d5 100644 --- a/server/utils/web/base_controller.py +++ b/server/utils/web/base_controller.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from typing import TYPE_CHECKING, Any, Awaitable, Optional import bcrypt From 32a50198fa11869afe188e7d271e91a4e4d1ad3b Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Tue, 27 Jan 2026 00:00:32 +0000 Subject: [PATCH 02/43] Add future annotations for TYPE_CHECKING compatibility (#881) Add `from __future__ import annotations` to settings.py and base_controller.py to enable proper deferred evaluation of type annotations. This is required when using TYPE_CHECKING imports to avoid runtime NameError when the imported types are used in function signatures. Co-authored-by: Claude Opus 4.5 --- server/digi_server/settings.py | 2 ++ server/utils/web/base_controller.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/server/digi_server/settings.py b/server/digi_server/settings.py index c2b8cf57..aface3a7 100644 --- a/server/digi_server/settings.py +++ b/server/digi_server/settings.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json import os import tomllib diff --git a/server/utils/web/base_controller.py b/server/utils/web/base_controller.py index 17f099d5..5aa3b422 100644 --- a/server/utils/web/base_controller.py +++ b/server/utils/web/base_controller.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import TYPE_CHECKING, Any, Awaitable, Optional import bcrypt From b6be4b835e24ae1770dfaf67fadfb7931836ba60 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Tue, 27 Jan 2026 00:04:26 +0000 Subject: [PATCH 03/43] Add server-side version checker (#878) (#880) Adds functionality for the server to check whether it is running the latest version of DigiScript, displayed in System Config -> System tab. Backend: - New VersionChecker service that queries GitHub Releases API - Checks on startup and periodically (every hour) - Caches results for fast API responses - New /api/v1/version/status and /api/v1/version/check endpoints Frontend: - Version row in System Config showing current version with status badge - Green "Up to date", yellow "Update Available", red "Unable to check" - "Check Now" button for manual refresh - Link to release notes when update is available Also fixes circular import issues by adding `from __future__ import annotations` to settings.py and base_controller.py. Closes #878 Co-authored-by: Claude Opus 4.5 --- .../vue_components/config/ConfigSystem.vue | 147 ++++++++++++ docs/pages/user_config.md | 19 ++ server/controllers/api/version.py | 60 +++++ server/digi_server/app_server.py | 16 ++ server/utils/version_checker.py | 218 ++++++++++++++++++ 5 files changed, 460 insertions(+) create mode 100644 server/controllers/api/version.py create mode 100644 server/utils/version_checker.py diff --git a/client/src/vue_components/config/ConfigSystem.vue b/client/src/vue_components/config/ConfigSystem.vue index 794ff48f..8c3342e4 100644 --- a/client/src/vue_components/config/ConfigSystem.vue +++ b/client/src/vue_components/config/ConfigSystem.vue @@ -38,6 +38,45 @@ + + + Version + + + {{ versionStatus.current_version || 'Unknown' }} + + {{ getVersionStatusText() }} + + + + + + + + Check Now + + + { + this.currentTime = Date.now(); + }, 1000); }, destroyed() { clearTimeout(this.clientTimeout); + if (this.timeUpdateInterval) { + clearInterval(this.timeUpdateInterval); + this.timeUpdateInterval = null; + } }, methods: { async getAvailableShows() { @@ -377,6 +437,93 @@ export default { this.isSubmittingLoad = false; } }, + async getVersionStatus() { + try { + const response = await fetch(`${makeURL('/api/v1/version/status')}`); + if (response.ok) { + this.versionStatus = await response.json(); + } else { + log.error('Unable to get version status'); + } + } catch (error) { + log.error('Error fetching version status:', error); + } + }, + async checkForUpdates() { + if (this.isCheckingVersion) { + return; + } + + this.isCheckingVersion = true; + + try { + const response = await fetch(`${makeURL('/api/v1/version/check')}`, { + method: 'POST', + }); + if (response.ok) { + this.versionStatus = await response.json(); + if (this.versionStatus.update_available) { + this.$toast.info(`Update available: ${this.versionStatus.latest_version}`); + } else if (!this.versionStatus.check_error) { + this.$toast.success('You are running the latest version'); + } + } else { + this.$toast.error('Unable to check for updates'); + log.error('Unable to check for updates'); + } + } catch (error) { + this.$toast.error('Unable to check for updates'); + log.error('Error checking for updates:', error); + } finally { + this.isCheckingVersion = false; + } + }, + getVersionStatusVariant() { + if (!this.versionStatus.current_version) { + return 'secondary'; + } + if (this.versionStatus.check_error) { + return 'danger'; + } + if (this.versionStatus.update_available) { + return 'warning'; + } + return 'success'; + }, + getVersionStatusText() { + if (!this.versionStatus.current_version) { + return 'Loading...'; + } + if (this.versionStatus.check_error) { + return 'Unable to check'; + } + if (this.versionStatus.update_available) { + return 'Update Available'; + } + return 'Up to date'; + }, + formatTimeAgo(isoTimestamp) { + if (!isoTimestamp) { + return 'Never'; + } + + const timestamp = new Date(isoTimestamp).getTime(); + const seconds = Math.floor((this.currentTime - timestamp) / 1000); + + if (seconds < 10) { + return 'Just now'; + } + if (seconds < 60) { + return `${seconds} seconds ago`; + } + if (seconds < 3600) { + return `${Math.floor(seconds / 60)} minutes ago`; + } + if (seconds < 86400) { + return `${Math.floor(seconds / 3600)} hours ago`; + } + return `${Math.floor(seconds / 86400)} days ago`; + }, ...mapMutations(['UPDATE_SHOWS']), ...mapActions(['GET_SCRIPT_MODES']), }, diff --git a/docs/pages/user_config.md b/docs/pages/user_config.md index 4bee4bba..7d734696 100644 --- a/docs/pages/user_config.md +++ b/docs/pages/user_config.md @@ -4,6 +4,25 @@ The **System Config** section, accessible from the top navigation bar, provides ![](../images/config_system/system_overview.png) +### System Tab + +The **System** tab provides an overview of the current system state: + +- **Current Show**: Displays the currently loaded show name, with buttons to load an existing show or set up a new one. +- **Connected Clients**: Shows the number of WebSocket clients currently connected to the server. Click "View Clients" to see details about each connected session. +- **Version**: Displays the current DigiScript version and checks for available updates. + +#### Version Checker + +The version checker automatically checks for new DigiScript releases when the server starts, and periodically (every hour) thereafter. The version status shows: + +- **Current version**: The version of DigiScript currently running +- **Status badge**: Indicates whether you're up to date (green), an update is available (yellow), or the check failed (red) +- **Latest version**: When an update is available, shows the newest version number with a link to the release notes +- **Last checked**: Timestamp of when the version was last checked + +Click the **Check Now** button to manually trigger a version check against the GitHub releases. + ### System Settings The **Settings** tab allows you to configure system-wide settings that apply across all shows: diff --git a/server/controllers/api/version.py b/server/controllers/api/version.py new file mode 100644 index 00000000..7d4a0364 --- /dev/null +++ b/server/controllers/api/version.py @@ -0,0 +1,60 @@ +from utils.web.base_controller import BaseAPIController +from utils.web.route import ApiRoute, ApiVersion +from utils.web.web_decorators import api_authenticated, require_admin + + +@ApiRoute("version/status", ApiVersion.V1) +class VersionStatusController(BaseAPIController): + @api_authenticated + async def get(self): + """ + Get the current version status. + + Returns information about the running version, latest available version, + whether an update is available, and when the last check occurred. + + :returns: JSON response with version status. + """ + version_checker = self.application.version_checker + if not version_checker: + await self.finish( + { + "error": "Version checker not initialized", + "current_version": None, + "latest_version": None, + "update_available": False, + "release_url": None, + "last_checked": None, + "check_error": "Service not available", + } + ) + return + + await self.finish(version_checker.status.as_json()) + + +@ApiRoute("version/check", ApiVersion.V1) +class VersionCheckController(BaseAPIController): + @api_authenticated + @require_admin + async def post(self): + """ + Trigger a manual version check. + + Forces an immediate check against the GitHub API, updating the + cached version status. + + :returns: JSON response with updated version status. + """ + version_checker = self.application.version_checker + if not version_checker: + self.set_status(503) + await self.finish( + { + "error": "Version checker not initialized", + } + ) + return + + status = await version_checker.check_for_updates() + await self.finish(status.as_json()) diff --git a/server/digi_server/app_server.py b/server/digi_server/app_server.py index 901e1673..8359ac09 100644 --- a/server/digi_server/app_server.py +++ b/server/digi_server/app_server.py @@ -32,6 +32,7 @@ from utils.exceptions import DatabaseTypeException, DatabaseUpgradeRequired from utils.mdns_service import MDNSAdvertiser from utils.module_discovery import get_resource_path, is_frozen +from utils.version_checker import VersionChecker from utils.web.jwt_service import JWTService from utils.web.route import Route @@ -60,6 +61,7 @@ def __init__( self._db: DigiSQLAlchemy = models.db self.jwt_service: JWTService = None self.mdns_advertiser: Optional[MDNSAdvertiser] = None + self.version_checker: Optional[VersionChecker] = None db_path: str = self.digi_settings.settings.get("db_path").get_value() if db_path.startswith("sqlite://"): @@ -351,6 +353,7 @@ def _check_migrations(self): async def configure(self): await self._configure_logging() await self.start_mdns_advertising() + await self.start_version_checker() async def _configure_logging(self): get_logger().info("Reconfiguring logging!") @@ -528,3 +531,16 @@ async def _toggle_mdns_advertising(self) -> None: else: # Stop advertising await self.stop_mdns_advertising() + + async def start_version_checker(self) -> None: + """Start the version checker service.""" + if not self.version_checker: + self.version_checker = VersionChecker(application=self) + + await self.version_checker.start() + + async def stop_version_checker(self) -> None: + """Stop the version checker service if it's running.""" + if self.version_checker: + await self.version_checker.stop() + self.version_checker = None diff --git a/server/utils/version_checker.py b/server/utils/version_checker.py new file mode 100644 index 00000000..f5488195 --- /dev/null +++ b/server/utils/version_checker.py @@ -0,0 +1,218 @@ +from __future__ import annotations + +import json +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Optional + +from tornado.httpclient import AsyncHTTPClient, HTTPClientError +from tornado.ioloop import PeriodicCallback + +from digi_server.logger import get_logger +from digi_server.settings import get_version + + +if TYPE_CHECKING: + from digi_server.app_server import DigiScriptServer + + +class VersionStatus: + def __init__( + self, + current_version: str, + latest_version: Optional[str] = None, + update_available: bool = False, + release_url: Optional[str] = None, + last_checked: Optional[datetime] = None, + check_error: Optional[str] = None, + ): + self.current_version = current_version + self.latest_version = latest_version + self.update_available = update_available + self.release_url = release_url + self.last_checked = last_checked + self.check_error = check_error + + def as_json(self) -> dict: + """ + Serialize version status to JSON-compatible dictionary. + + :returns: Dictionary with version status fields. + """ + return { + "current_version": self.current_version, + "latest_version": self.latest_version, + "update_available": self.update_available, + "release_url": self.release_url, + "last_checked": ( + self.last_checked.isoformat() if self.last_checked else None + ), + "check_error": self.check_error, + } + + +class VersionChecker: + GITHUB_API_URL = ( + "https://api.github.com/repos/dreamteamprod/DigiScript/releases/latest" + ) + DEFAULT_CHECK_INTERVAL_MS = 60 * 60 * 1000 + + def __init__( + self, + application: "DigiScriptServer", + check_interval_ms: Optional[int] = None, + ): + """ + Initialize the version checker. + + :param application: The DigiScript server application instance. + :param check_interval_ms: Interval between checks in milliseconds. + Defaults to 1 hour. + """ + self._application = application + self._check_interval_ms = check_interval_ms or self.DEFAULT_CHECK_INTERVAL_MS + self._logger = get_logger(name="version_checker") + self._http_client = AsyncHTTPClient() + self._periodic_callback: Optional[PeriodicCallback] = None + + # Initialize status with current version + self._status = VersionStatus(current_version=get_version()) + + @property + def status(self) -> VersionStatus: + """Get the current version status.""" + return self._status + + async def start(self) -> None: + """ + Start the version checker. + + Performs an initial check and schedules periodic checks. + """ + self._logger.info("Starting version checker service") + + # Perform initial check + await self.check_for_updates() + + # Schedule periodic checks + self._periodic_callback = PeriodicCallback( + self.check_for_updates, + self._check_interval_ms, + ) + self._periodic_callback.start() + + self._logger.info( + f"Version checker started (interval: {self._check_interval_ms // 1000}s)" + ) + + async def stop(self) -> None: + """Stop the version checker and cancel periodic checks.""" + if self._periodic_callback: + self._periodic_callback.stop() + self._periodic_callback = None + + if self._http_client: + self._http_client.close() + self._http_client = None + + self._logger.info("Version checker stopped") + + async def check_for_updates(self) -> VersionStatus: + """ + Check GitHub for the latest release version. + + :returns: Updated VersionStatus with check results. + """ + self._logger.debug("Checking for updates...") + + current_version = get_version() + + try: + response = await self._http_client.fetch( + self.GITHUB_API_URL, + headers={ + "Accept": "application/vnd.github.v3+json", + "User-Agent": f"DigiScript/{current_version}", + }, + request_timeout=10.0, + ) + + release_data = json.loads(response.body.decode("utf-8")) + latest_version = release_data.get("tag_name", "").lstrip("v") + release_url = release_data.get("html_url") + + # Compare versions + update_available = self._is_newer_version(latest_version, current_version) + + self._status = VersionStatus( + current_version=current_version, + latest_version=latest_version, + update_available=update_available, + release_url=release_url, + last_checked=datetime.now(timezone.utc), + check_error=None, + ) + + if update_available: + self._logger.info( + f"Update available: {current_version} -> {latest_version}" + ) + else: + self._logger.debug(f"Running latest version: {current_version}") + + except HTTPClientError as e: + error_msg = f"HTTP error checking for updates: {e.code}" + self._logger.warning(error_msg) + self._status = VersionStatus( + current_version=current_version, + latest_version=self._status.latest_version, + update_available=self._status.update_available, + release_url=self._status.release_url, + last_checked=datetime.now(timezone.utc), + check_error=error_msg, + ) + + except Exception as e: + error_msg = f"Unable to check for updates: {str(e)}" + self._logger.warning(error_msg) + self._status = VersionStatus( + current_version=current_version, + latest_version=self._status.latest_version, + update_available=self._status.update_available, + release_url=self._status.release_url, + last_checked=datetime.now(timezone.utc), + check_error=error_msg, + ) + + return self._status + + def _is_newer_version(self, latest: str, current: str) -> bool: + """ + Compare version strings to determine if an update is available. + + Uses simple semantic version comparison (major.minor.patch). + Handles pre-release suffixes (e.g., "1.0.0-beta") by stripping them. + + :param latest: The latest version string from GitHub. + :param current: The current running version string. + :returns: True if latest is newer than current. + """ + try: + # Strip pre-release suffixes (everything after -) + latest_clean = latest.split("-")[0] + current_clean = current.split("-")[0] + + latest_parts = [int(x) for x in latest_clean.split(".")] + current_parts = [int(x) for x in current_clean.split(".")] + + # Pad shorter version with zeros + max_len = max(len(latest_parts), len(current_parts)) + latest_parts.extend([0] * (max_len - len(latest_parts))) + current_parts.extend([0] * (max_len - len(current_parts))) + + return latest_parts > current_parts + except (ValueError, AttributeError): + # If parsing fails, assume no update available + self._logger.warning( + f"Failed to parse versions: latest={latest}, current={current}" + ) + return False From 0dd065008440815e9fd48e649cdda11dd21e07e5 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Wed, 28 Jan 2026 00:43:31 +0000 Subject: [PATCH 04/43] Standardize on `from __future__ import annotations` and unquote type annotations (#882) Add PEP 563 deferred annotation evaluation to all files that use TYPE_CHECKING or string-quoted forward references, then remove the now-unnecessary string quotes from Mapped[] type annotations and function parameter/return type annotations. Co-authored-by: Claude Opus 4.5 --- server/controllers/ws_controller.py | 2 ++ server/models/cue.py | 14 ++++---- server/models/mics.py | 10 +++--- server/models/script.py | 54 +++++++++++++++-------------- server/models/session.py | 20 ++++++----- server/models/show.py | 52 +++++++++++++-------------- server/models/user.py | 4 ++- server/rbac/rbac.py | 4 ++- server/rbac/rbac_db.py | 4 ++- server/registry/schema.py | 4 ++- 10 files changed, 92 insertions(+), 76 deletions(-) diff --git a/server/controllers/ws_controller.py b/server/controllers/ws_controller.py index bfa38e41..d202f753 100644 --- a/server/controllers/ws_controller.py +++ b/server/controllers/ws_controller.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime import json from typing import TYPE_CHECKING, Any, Awaitable, Dict, Optional, Union diff --git a/server/models/cue.py b/server/models/cue.py index b2658ece..ced32185 100644 --- a/server/models/cue.py +++ b/server/models/cue.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import List from sqlalchemy import ForeignKey, String, func, select @@ -20,7 +22,7 @@ class CueType(db.Model, DeleteMixin): description: Mapped[str | None] = mapped_column(String(100)) colour: Mapped[str | None] = mapped_column(String(16)) - cues: Mapped[List["Cue"]] = relationship( + cues: Mapped[List[Cue]] = relationship( back_populates="cue_type", cascade="all, delete-orphan" ) @@ -38,10 +40,10 @@ class Cue(db.Model): cue_type_id: Mapped[int | None] = mapped_column(ForeignKey("cuetypes.id")) ident: Mapped[str | None] = mapped_column(String(50)) - cue_type: Mapped["CueType"] = relationship( + cue_type: Mapped[CueType] = relationship( foreign_keys=[cue_type_id], back_populates="cues" ) - revision_associations: Mapped[List["CueAssociation"]] = relationship( + revision_associations: Mapped[List[CueAssociation]] = relationship( cascade="all, delete-orphan", back_populates="cue" ) @@ -59,13 +61,13 @@ class CueAssociation(db.Model, DeleteMixin): ForeignKey("cue.id"), primary_key=True, index=True ) - revision: Mapped["ScriptRevision"] = relationship( + revision: Mapped[ScriptRevision] = relationship( foreign_keys=[revision_id], back_populates="cue_associations" ) - line: Mapped["ScriptLine"] = relationship( + line: Mapped[ScriptLine] = relationship( foreign_keys=[line_id], back_populates="cue_associations", viewonly=True ) - cue: Mapped["Cue"] = relationship( + cue: Mapped[Cue] = relationship( foreign_keys=[cue_id], back_populates="revision_associations" ) diff --git a/server/models/mics.py b/server/models/mics.py index b9a65511..2fce6f03 100644 --- a/server/models/mics.py +++ b/server/models/mics.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import TYPE_CHECKING, List from sqlalchemy import ForeignKey, String @@ -19,7 +21,7 @@ class Microphone(db.Model): name: Mapped[str | None] = mapped_column(String(100)) description: Mapped[str | None] = mapped_column(String(500)) - allocations: Mapped[List["MicrophoneAllocation"]] = relationship( + allocations: Mapped[List[MicrophoneAllocation]] = relationship( cascade="all, delete-orphan", back_populates="microphone" ) @@ -33,6 +35,6 @@ class MicrophoneAllocation(db.Model): ForeignKey("character.id"), primary_key=True ) - microphone: Mapped["Microphone"] = relationship(back_populates="allocations") - scene: Mapped["Scene"] = relationship(back_populates="mic_allocations") - character: Mapped["Character"] = relationship(back_populates="mic_allocations") + microphone: Mapped[Microphone] = relationship(back_populates="allocations") + scene: Mapped[Scene] = relationship(back_populates="mic_allocations") + character: Mapped[Character] = relationship(back_populates="mic_allocations") diff --git a/server/models/script.py b/server/models/script.py index d1f3a97d..414b4aaf 100644 --- a/server/models/script.py +++ b/server/models/script.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime import enum import gzip @@ -41,11 +43,11 @@ class Script(db.Model): ForeignKey("script_revisions.id") ) - revisions: Mapped[List["ScriptRevision"]] = relationship( + revisions: Mapped[List[ScriptRevision]] = relationship( primaryjoin="ScriptRevision.script_id == Script.id", back_populates="script" ) - show: Mapped["Show"] = relationship(foreign_keys=[show_id]) - stage_direction_styles: Mapped[List["StageDirectionStyle"]] = relationship( + show: Mapped[Show] = relationship(foreign_keys=[show_id]) + stage_direction_styles: Mapped[List[StageDirectionStyle]] = relationship( cascade="all, delete-orphan", back_populates="script" ) @@ -64,19 +66,19 @@ class ScriptRevision(db.Model): ForeignKey("script_revisions.id", ondelete="SET NULL") ) - previous_revision: Mapped["ScriptRevision"] = relationship( + previous_revision: Mapped[ScriptRevision] = relationship( foreign_keys=[previous_revision_id] ) - script: Mapped["Script"] = relationship( + script: Mapped[Script] = relationship( foreign_keys=[script_id], back_populates="revisions" ) - line_associations: Mapped[List["ScriptLineRevisionAssociation"]] = relationship( + line_associations: Mapped[List[ScriptLineRevisionAssociation]] = relationship( cascade="all, delete", back_populates="revision" ) - line_part_cuts: Mapped[List["ScriptCuts"]] = relationship( + line_part_cuts: Mapped[List[ScriptCuts]] = relationship( cascade="all, delete-orphan", back_populates="revision" ) - cue_associations: Mapped[List["CueAssociation"]] = relationship( + cue_associations: Mapped[List[CueAssociation]] = relationship( cascade="all, delete-orphan", back_populates="revision" ) @@ -130,17 +132,17 @@ class ScriptLine(db.Model): ForeignKey("stage_direction_styles.id", ondelete="SET NULL") ) - act: Mapped["Act"] = relationship(back_populates="lines") - scene: Mapped["Scene"] = relationship(back_populates="lines") - revision_associations: Mapped[List["ScriptLineRevisionAssociation"]] = relationship( + act: Mapped[Act] = relationship(back_populates="lines") + scene: Mapped[Scene] = relationship(back_populates="lines") + revision_associations: Mapped[List[ScriptLineRevisionAssociation]] = relationship( foreign_keys="[ScriptLineRevisionAssociation.line_id]", cascade="all, delete", back_populates="line", ) - cue_associations: Mapped[List["CueAssociation"]] = relationship( + cue_associations: Mapped[List[CueAssociation]] = relationship( foreign_keys="[CueAssociation.line_id]", viewonly=True, back_populates="line" ) - line_parts: Mapped[List["ScriptLinePart"]] = relationship( + line_parts: Mapped[List[ScriptLinePart]] = relationship( cascade="all, delete-orphan", back_populates="line" ) @@ -158,14 +160,14 @@ class ScriptLineRevisionAssociation(db.Model, DeleteMixin): next_line_id: Mapped[int | None] = mapped_column(ForeignKey("script_lines.id")) previous_line_id: Mapped[int | None] = mapped_column(ForeignKey("script_lines.id")) - revision: Mapped["ScriptRevision"] = relationship( + revision: Mapped[ScriptRevision] = relationship( foreign_keys=[revision_id], back_populates="line_associations" ) - line: Mapped["ScriptLine"] = relationship( + line: Mapped[ScriptLine] = relationship( foreign_keys=[line_id], back_populates="revision_associations" ) - next_line: Mapped["ScriptLine"] = relationship(foreign_keys=[next_line_id]) - previous_line: Mapped["ScriptLine"] = relationship(foreign_keys=[previous_line_id]) + next_line: Mapped[ScriptLine] = relationship(foreign_keys=[next_line_id]) + previous_line: Mapped[ScriptLine] = relationship(foreign_keys=[previous_line_id]) def pre_delete(self, session): pass @@ -259,12 +261,12 @@ class ScriptLinePart(db.Model): ) line_text: Mapped[str | None] = mapped_column(String) - line: Mapped["ScriptLine"] = relationship( + line: Mapped[ScriptLine] = relationship( foreign_keys=[line_id], back_populates="line_parts" ) - character: Mapped["Character"] = relationship() - character_group: Mapped["CharacterGroup"] = relationship() - line_part_cuts: Mapped["ScriptCuts"] = relationship( + character: Mapped[Character] = relationship() + character_group: Mapped[CharacterGroup] = relationship() + line_part_cuts: Mapped[ScriptCuts] = relationship( cascade="all, delete-orphan", back_populates="line_part" ) @@ -279,10 +281,10 @@ class ScriptCuts(db.Model): ForeignKey("script_revisions.id"), primary_key=True, index=True ) - line_part: Mapped["ScriptLinePart"] = relationship( + line_part: Mapped[ScriptLinePart] = relationship( foreign_keys=[line_part_id], back_populates="line_part_cuts" ) - revision: Mapped["ScriptRevision"] = relationship( + revision: Mapped[ScriptRevision] = relationship( foreign_keys=[revision_id], back_populates="line_part_cuts" ) @@ -313,7 +315,7 @@ class StageDirectionStyle(db.Model, DeleteMixin): enable_background_colour: Mapped[bool | None] = mapped_column(Boolean) background_colour: Mapped[str | None] = mapped_column(String) - script: Mapped["Script"] = relationship( + script: Mapped[Script] = relationship( foreign_keys=[script_id], back_populates="stage_direction_styles" ) @@ -344,10 +346,10 @@ class CompiledScript(db.Model): ) data_path: Mapped[str | None] = mapped_column(String) - script_revision: Mapped["ScriptRevision"] = relationship(foreign_keys=[revision_id]) + script_revision: Mapped[ScriptRevision] = relationship(foreign_keys=[revision_id]) @classmethod - async def compile_script(cls, application: "DigiScriptServer", revision_id): + async def compile_script(cls, application: DigiScriptServer, revision_id): line_schema = get_registry().get_schema_by_model(ScriptLine)() with application.get_db().sessionmaker() as session: revision: ScriptRevision = session.get(ScriptRevision, revision_id) diff --git a/server/models/session.py b/server/models/session.py index cc72e09d..416eebe9 100644 --- a/server/models/session.py +++ b/server/models/session.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime from functools import partial from typing import TYPE_CHECKING @@ -26,8 +28,8 @@ class Session(db.Model): ForeignKey("user.id", ondelete="SET NULL"), index=True ) - user: Mapped["User"] = relationship(back_populates="sessions") - live_session: Mapped["ShowSession"] = relationship( + user: Mapped[User] = relationship(back_populates="sessions") + live_session: Mapped[ShowSession] = relationship( foreign_keys="[ShowSession.client_internal_id]", back_populates="client", ) @@ -66,16 +68,16 @@ class ShowSession(db.Model): ForeignKey("showinterval.id", ondelete="SET NULL") ) - show: Mapped["Show"] = relationship(uselist=False, foreign_keys=[show_id]) - revision: Mapped["ScriptRevision"] = relationship( + show: Mapped[Show] = relationship(uselist=False, foreign_keys=[show_id]) + revision: Mapped[ScriptRevision] = relationship( uselist=False, foreign_keys=[script_revision_id] ) - user: Mapped["User"] = relationship(uselist=False, foreign_keys=[user_id]) - client: Mapped["Session"] = relationship( + user: Mapped[User] = relationship(uselist=False, foreign_keys=[user_id]) + client: Mapped[Session] = relationship( foreign_keys=[client_internal_id], back_populates="live_session", ) - tags: Mapped[list["SessionTag"]] = relationship( + tags: Mapped[list[SessionTag]] = relationship( secondary=session_tag_association_table, back_populates="sessions" ) @@ -105,7 +107,7 @@ class SessionTag(db.Model): tag: Mapped[str] = mapped_column(String(255)) colour: Mapped[str] = mapped_column(String(16)) - show: Mapped["Show"] = relationship(uselist=False, foreign_keys=[show_id]) - sessions: Mapped[list["ShowSession"]] = relationship( + show: Mapped[Show] = relationship(uselist=False, foreign_keys=[show_id]) + sessions: Mapped[list[ShowSession]] = relationship( secondary=session_tag_association_table, back_populates="tags" ) diff --git a/server/models/show.py b/server/models/show.py index 5c115844..ce739b63 100644 --- a/server/models/show.py +++ b/server/models/show.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime import enum from typing import TYPE_CHECKING, List @@ -60,23 +62,21 @@ class Show(db.Model): script_mode: Mapped[ShowScriptType] = mapped_column(ShowScriptTypeCol) # Relationships - first_act: Mapped["Act"] = relationship(foreign_keys=[first_act_id]) - current_session: Mapped["ShowSession"] = relationship( + first_act: Mapped[Act] = relationship(foreign_keys=[first_act_id]) + current_session: Mapped[ShowSession] = relationship( foreign_keys=[current_session_id] ) - cast_list: Mapped[List["Cast"]] = relationship(cascade="all, delete-orphan") - character_list: Mapped[List["Character"]] = relationship( - cascade="all, delete-orphan" - ) - character_group_list: Mapped[List["CharacterGroup"]] = relationship( + cast_list: Mapped[List[Cast]] = relationship(cascade="all, delete-orphan") + character_list: Mapped[List[Character]] = relationship(cascade="all, delete-orphan") + character_group_list: Mapped[List[CharacterGroup]] = relationship( cascade="all, delete-orphan" ) - act_list: Mapped[List["Act"]] = relationship( + act_list: Mapped[List[Act]] = relationship( primaryjoin="Show.id == Act.show_id", cascade="all, delete-orphan" ) - scene_list: Mapped[List["Scene"]] = relationship(cascade="all, delete-orphan") - cue_type_list: Mapped[List["CueType"]] = relationship(cascade="all, delete-orphan") + scene_list: Mapped[List[Scene]] = relationship(cascade="all, delete-orphan") + cue_type_list: Mapped[List[CueType]] = relationship(cascade="all, delete-orphan") class Cast(db.Model): @@ -88,9 +88,7 @@ class Cast(db.Model): last_name: Mapped[str | None] = mapped_column() # Relationships - character_list: Mapped[List["Character"]] = relationship( - back_populates="cast_member" - ) + character_list: Mapped[List[Character]] = relationship(back_populates="cast_member") character_group_association_table = Table( @@ -110,12 +108,12 @@ class Character(db.Model): name: Mapped[str | None] = mapped_column() description: Mapped[str | None] = mapped_column() - cast_member: Mapped["Cast"] = relationship(back_populates="character_list") - character_groups: Mapped[List["CharacterGroup"]] = relationship( + cast_member: Mapped[Cast] = relationship(back_populates="character_list") + character_groups: Mapped[List[CharacterGroup]] = relationship( secondary=character_group_association_table, back_populates="characters", ) - mic_allocations: Mapped[List["MicrophoneAllocation"]] = relationship( + mic_allocations: Mapped[List[MicrophoneAllocation]] = relationship( cascade="all, delete-orphan", back_populates="character" ) @@ -129,7 +127,7 @@ class CharacterGroup(db.Model): name: Mapped[str | None] = mapped_column() description: Mapped[str | None] = mapped_column() - characters: Mapped[List["Character"]] = relationship( + characters: Mapped[List[Character]] = relationship( secondary=character_group_association_table, back_populates="character_groups", ) @@ -145,22 +143,22 @@ class Act(db.Model): first_scene_id: Mapped[int | None] = mapped_column(ForeignKey("scene.id")) previous_act_id: Mapped[int | None] = mapped_column(ForeignKey("act.id")) - first_scene: Mapped["Scene"] = relationship(foreign_keys=[first_scene_id]) - previous_act: Mapped["Act"] = relationship( + first_scene: Mapped[Scene] = relationship(foreign_keys=[first_scene_id]) + previous_act: Mapped[Act] = relationship( remote_side="[Act.id]", back_populates="next_act", foreign_keys=[previous_act_id], ) - next_act: Mapped["Act"] = relationship( + next_act: Mapped[Act] = relationship( back_populates="previous_act", foreign_keys="[Act.previous_act_id]", ) - scene_list: Mapped[List["Scene"]] = relationship( + scene_list: Mapped[List[Scene]] = relationship( back_populates="act", cascade="all, delete-orphan", foreign_keys="[Scene.act_id]", ) - lines: Mapped[List["ScriptLine"]] = relationship( + lines: Mapped[List[ScriptLine]] = relationship( back_populates="act", cascade="all, delete-orphan" ) @@ -174,23 +172,23 @@ class Scene(db.Model): name: Mapped[str | None] = mapped_column() previous_scene_id: Mapped[int | None] = mapped_column(ForeignKey("scene.id")) - act: Mapped["Act"] = relationship( + act: Mapped[Act] = relationship( back_populates="scene_list", foreign_keys=[act_id], post_update=True, ) - previous_scene: Mapped["Scene"] = relationship( + previous_scene: Mapped[Scene] = relationship( remote_side="[Scene.id]", back_populates="next_scene", foreign_keys=[previous_scene_id], ) - next_scene: Mapped["Scene"] = relationship( + next_scene: Mapped[Scene] = relationship( back_populates="previous_scene", foreign_keys="[Scene.previous_scene_id]", ) - lines: Mapped[List["ScriptLine"]] = relationship( + lines: Mapped[List[ScriptLine]] = relationship( back_populates="scene", cascade="all, delete-orphan" ) - mic_allocations: Mapped[List["MicrophoneAllocation"]] = relationship( + mic_allocations: Mapped[List[MicrophoneAllocation]] = relationship( cascade="all, delete-orphan", back_populates="scene" ) diff --git a/server/models/user.py b/server/models/user.py index 5075859e..aae2b45f 100644 --- a/server/models/user.py +++ b/server/models/user.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime import enum import json @@ -71,7 +73,7 @@ class User(db.Model): requires_password_change: Mapped[bool] = mapped_column(default=False) token_version: Mapped[int] = mapped_column(default=0) - sessions: Mapped[List["Session"]] = relationship(back_populates="user") + sessions: Mapped[List[Session]] = relationship(back_populates="user") class UserSettings(db.Model): diff --git a/server/rbac/rbac.py b/server/rbac/rbac.py index a3fa8b9c..c5074782 100644 --- a/server/rbac/rbac.py +++ b/server/rbac/rbac.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import TYPE_CHECKING, List, Optional from models.models import db @@ -12,7 +14,7 @@ class RBACController: - def __init__(self, app: "DigiScriptServer"): + def __init__(self, app: DigiScriptServer): self.app = app self._rbac_db = RBACDatabase(app.get_db(), app) self._display_fields = {} diff --git a/server/rbac/rbac_db.py b/server/rbac/rbac_db.py index 4fa413cf..19151ab4 100644 --- a/server/rbac/rbac_db.py +++ b/server/rbac/rbac_db.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import functools from collections import defaultdict from copy import deepcopy @@ -85,7 +87,7 @@ def _get_mapping_columns( class RBACDatabase: - def __init__(self, _db: DigiSQLAlchemy, app: "DigiScriptServer"): + def __init__(self, _db: DigiSQLAlchemy, app: DigiScriptServer): self._db: DigiSQLAlchemy = _db self._app = app self._mappings = {} diff --git a/server/registry/schema.py b/server/registry/schema.py index 4e5f7f53..9e744e6f 100644 --- a/server/registry/schema.py +++ b/server/registry/schema.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import TYPE_CHECKING, Optional from marshmallow_sqlalchemy import SQLAlchemySchema @@ -19,7 +21,7 @@ def set(self, key, value): def get_schema_by_model(self, key) -> Optional[SQLAlchemySchema]: return self._backward_registry.get(key, None) - def get_model_by_schema(self, key) -> "db.Model": + def get_model_by_schema(self, key) -> db.Model: return self._forward_registry.get(key, None) From 631403357df7695fdbd5f3b5702b1fd6e526170a Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Thu, 29 Jan 2026 00:16:21 +0000 Subject: [PATCH 05/43] Stage & Crew features (#692) --- client/package-lock.json | 7 + client/package.json | 1 + client/src/App.vue | 15 +- client/src/assets/styles/dark.scss | 8 + client/src/main.js | 4 + client/src/router/index.js | 6 + client/src/store/modules/show.js | 7 + client/src/store/modules/stage.js | 482 +++++++ client/src/store/store.js | 4 +- client/src/views/show/ShowConfigView.vue | 9 + client/src/views/show/ShowLiveView.vue | 1123 +---------------- client/src/views/show/config/ConfigStage.vue | 39 + .../show/config/stage/CrewList.vue | 220 ++++ .../show/config/stage/PropsList.vue | 489 +++++++ .../show/config/stage/SceneryList.vue | 497 ++++++++ .../show/config/stage/StageManager.vue | 518 ++++++++ .../show/live/ScriptViewPane.vue | 1122 ++++++++++++++++ .../show/live/StageManagerPane.vue | 426 +++++++ ...8_add_unique_constraints_to_allocation_.py | 42 + ...9849eb6d381a_add_prop_and_scenery_types.py | 93 ++ ..._add_crew_props_and_scenery_tables_and_.py | 105 ++ server/controllers/api/show/stage/__init__.py | 0 server/controllers/api/show/stage/crew.py | 162 +++ server/controllers/api/show/stage/props.py | 504 ++++++++ server/controllers/api/show/stage/scenery.py | 517 ++++++++ server/models/show.py | 38 +- server/models/stage.py | 124 ++ server/schemas/schemas.py | 65 + .../controllers/api/show/stage/__init__.py | 0 .../controllers/api/show/stage/test_crew.py | 288 +++++ .../controllers/api/show/stage/test_props.py | 1058 ++++++++++++++++ .../api/show/stage/test_scenery.py | 1080 ++++++++++++++++ 32 files changed, 7970 insertions(+), 1083 deletions(-) create mode 100644 client/src/store/modules/stage.js create mode 100644 client/src/views/show/config/ConfigStage.vue create mode 100644 client/src/vue_components/show/config/stage/CrewList.vue create mode 100644 client/src/vue_components/show/config/stage/PropsList.vue create mode 100644 client/src/vue_components/show/config/stage/SceneryList.vue create mode 100644 client/src/vue_components/show/config/stage/StageManager.vue create mode 100644 client/src/vue_components/show/live/ScriptViewPane.vue create mode 100644 client/src/vue_components/show/live/StageManagerPane.vue create mode 100644 server/alembic_config/versions/625ac1e96e88_add_unique_constraints_to_allocation_.py create mode 100644 server/alembic_config/versions/9849eb6d381a_add_prop_and_scenery_types.py create mode 100644 server/alembic_config/versions/fa27b233d26c_add_crew_props_and_scenery_tables_and_.py create mode 100644 server/controllers/api/show/stage/__init__.py create mode 100644 server/controllers/api/show/stage/crew.py create mode 100644 server/controllers/api/show/stage/props.py create mode 100644 server/controllers/api/show/stage/scenery.py create mode 100644 server/models/stage.py create mode 100644 server/test/controllers/api/show/stage/__init__.py create mode 100644 server/test/controllers/api/show/stage/test_crew.py create mode 100644 server/test/controllers/api/show/stage/test_props.py create mode 100644 server/test/controllers/api/show/stage/test_scenery.py diff --git a/client/package-lock.json b/client/package-lock.json index a93003c0..629e1457 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -23,6 +23,7 @@ "lodash": "4.17.23", "loglevel": "1.9.2", "marked": "11.2.0", + "splitpanes": "^2.4.1", "vue": "2.7.14", "vue-multiselect": "2.1.9", "vue-native-websocket": "2.0.15", @@ -6268,6 +6269,12 @@ "node": ">=0.10.0" } }, + "node_modules/splitpanes": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/splitpanes/-/splitpanes-2.4.1.tgz", + "integrity": "sha512-kpEo1WuMXuc6QfdQdO2V/fl/trONlkUKp+pputsLTiW9RMtwEvjb4/aYGm2m3+KAzjmb+zLwr4A4SYZu74+pgQ==", + "license": "MIT" + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", diff --git a/client/package.json b/client/package.json index a1d43e4f..45e612e0 100644 --- a/client/package.json +++ b/client/package.json @@ -41,6 +41,7 @@ "lodash": "4.17.23", "loglevel": "1.9.2", "marked": "11.2.0", + "splitpanes": "^2.4.1", "vue": "2.7.14", "vue-multiselect": "2.1.9", "vue-native-websocket": "2.0.15", diff --git a/client/src/App.vue b/client/src/App.vue index 068f974d..27fe71f4 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -60,6 +60,17 @@ > Jump To Page + + {{ STAGE_MANAGER_MODE ? 'Disable' : 'Enable' }} Stage Manager + diff --git a/client/src/views/show/config/ConfigStage.vue b/client/src/views/show/config/ConfigStage.vue new file mode 100644 index 00000000..4dc317c3 --- /dev/null +++ b/client/src/views/show/config/ConfigStage.vue @@ -0,0 +1,39 @@ + + + diff --git a/client/src/vue_components/show/config/stage/CrewList.vue b/client/src/vue_components/show/config/stage/CrewList.vue new file mode 100644 index 00000000..d1c0b566 --- /dev/null +++ b/client/src/vue_components/show/config/stage/CrewList.vue @@ -0,0 +1,220 @@ + + + diff --git a/client/src/vue_components/show/config/stage/PropsList.vue b/client/src/vue_components/show/config/stage/PropsList.vue new file mode 100644 index 00000000..ef0b82bf --- /dev/null +++ b/client/src/vue_components/show/config/stage/PropsList.vue @@ -0,0 +1,489 @@ + + + diff --git a/client/src/vue_components/show/config/stage/SceneryList.vue b/client/src/vue_components/show/config/stage/SceneryList.vue new file mode 100644 index 00000000..1ca44dab --- /dev/null +++ b/client/src/vue_components/show/config/stage/SceneryList.vue @@ -0,0 +1,497 @@ + + + diff --git a/client/src/vue_components/show/config/stage/StageManager.vue b/client/src/vue_components/show/config/stage/StageManager.vue new file mode 100644 index 00000000..693189b0 --- /dev/null +++ b/client/src/vue_components/show/config/stage/StageManager.vue @@ -0,0 +1,518 @@ + + + + + diff --git a/client/src/vue_components/show/live/ScriptViewPane.vue b/client/src/vue_components/show/live/ScriptViewPane.vue new file mode 100644 index 00000000..e5d76612 --- /dev/null +++ b/client/src/vue_components/show/live/ScriptViewPane.vue @@ -0,0 +1,1122 @@ + + + + + diff --git a/client/src/vue_components/show/live/StageManagerPane.vue b/client/src/vue_components/show/live/StageManagerPane.vue new file mode 100644 index 00000000..75341f58 --- /dev/null +++ b/client/src/vue_components/show/live/StageManagerPane.vue @@ -0,0 +1,426 @@ + + + + + diff --git a/server/alembic_config/versions/625ac1e96e88_add_unique_constraints_to_allocation_.py b/server/alembic_config/versions/625ac1e96e88_add_unique_constraints_to_allocation_.py new file mode 100644 index 00000000..e9bb6c6c --- /dev/null +++ b/server/alembic_config/versions/625ac1e96e88_add_unique_constraints_to_allocation_.py @@ -0,0 +1,42 @@ +"""add unique constraints to allocation tables + +Revision ID: 625ac1e96e88 +Revises: 9849eb6d381a +Create Date: 2026-01-16 00:31:42.000334 + +""" + +from typing import Sequence, Union + +from alembic import op + + +# revision identifiers, used by Alembic. +revision: str = "625ac1e96e88" +down_revision: Union[str, None] = "9849eb6d381a" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("props_allocation", schema=None) as batch_op: + batch_op.create_unique_constraint("uq_props_scene", ["props_id", "scene_id"]) + + with op.batch_alter_table("scenery_allocation", schema=None) as batch_op: + batch_op.create_unique_constraint( + "uq_scenery_scene", ["scenery_id", "scene_id"] + ) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("scenery_allocation", schema=None) as batch_op: + batch_op.drop_constraint("uq_scenery_scene", type_="unique") + + with op.batch_alter_table("props_allocation", schema=None) as batch_op: + batch_op.drop_constraint("uq_props_scene", type_="unique") + + # ### end Alembic commands ### diff --git a/server/alembic_config/versions/9849eb6d381a_add_prop_and_scenery_types.py b/server/alembic_config/versions/9849eb6d381a_add_prop_and_scenery_types.py new file mode 100644 index 00000000..22f8f34c --- /dev/null +++ b/server/alembic_config/versions/9849eb6d381a_add_prop_and_scenery_types.py @@ -0,0 +1,93 @@ +"""Add prop and scenery types + +Revision ID: 9849eb6d381a +Revises: fa27b233d26c +Create Date: 2026-01-14 01:01:31.586812 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision: str = "9849eb6d381a" +down_revision: Union[str, None] = "fa27b233d26c" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "prop_type", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("show_id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["show_id"], ["shows.id"], name=op.f("fk_prop_type_show_id_shows") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_prop_type")), + ) + op.create_table( + "scenery_type", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("show_id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["show_id"], ["shows.id"], name=op.f("fk_scenery_type_show_id_shows") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_scenery_type")), + ) + with op.batch_alter_table("crew", schema=None) as batch_op: + batch_op.alter_column("first_name", existing_type=sa.String(), nullable=False) + + with op.batch_alter_table("props", schema=None) as batch_op: + batch_op.add_column(sa.Column("prop_type_id", sa.Integer(), nullable=False)) + batch_op.alter_column("name", existing_type=sa.String(), nullable=False) + batch_op.create_foreign_key( + batch_op.f("fk_props_prop_type_id_prop_type"), + "prop_type", + ["prop_type_id"], + ["id"], + ) + + with op.batch_alter_table("scenery", schema=None) as batch_op: + batch_op.add_column(sa.Column("scenery_type_id", sa.Integer(), nullable=False)) + batch_op.alter_column("name", existing_type=sa.String(), nullable=False) + batch_op.create_foreign_key( + batch_op.f("fk_scenery_scenery_type_id_scenery_type"), + "scenery_type", + ["scenery_type_id"], + ["id"], + ) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("scenery", schema=None) as batch_op: + batch_op.drop_constraint( + batch_op.f("fk_scenery_scenery_type_id_scenery_type"), type_="foreignkey" + ) + batch_op.alter_column("name", existing_type=sa.String(), nullable=True) + batch_op.drop_column("scenery_type_id") + + with op.batch_alter_table("props", schema=None) as batch_op: + batch_op.drop_constraint( + batch_op.f("fk_props_prop_type_id_prop_type"), type_="foreignkey" + ) + batch_op.alter_column("name", existing_type=sa.String(), nullable=True) + batch_op.drop_column("prop_type_id") + + with op.batch_alter_table("crew", schema=None) as batch_op: + batch_op.alter_column("first_name", existing_type=sa.String(), nullable=True) + + op.drop_table("scenery_type") + op.drop_table("prop_type") + # ### end Alembic commands ### diff --git a/server/alembic_config/versions/fa27b233d26c_add_crew_props_and_scenery_tables_and_.py b/server/alembic_config/versions/fa27b233d26c_add_crew_props_and_scenery_tables_and_.py new file mode 100644 index 00000000..920b08ba --- /dev/null +++ b/server/alembic_config/versions/fa27b233d26c_add_crew_props_and_scenery_tables_and_.py @@ -0,0 +1,105 @@ +"""Add crew, props and scenery tables and allocations + +Revision ID: fa27b233d26c +Revises: 01fb1d6c6b08 +Create Date: 2026-01-14 00:38:40.210710 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision: str = "fa27b233d26c" +down_revision: Union[str, None] = "01fb1d6c6b08" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "crew", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("show_id", sa.Integer(), nullable=False), + sa.Column("first_name", sa.String(), nullable=True), + sa.Column("last_name", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["show_id"], ["shows.id"], name=op.f("fk_crew_show_id_shows") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_crew")), + ) + op.create_table( + "props", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("show_id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(), nullable=True), + sa.Column("description", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["show_id"], ["shows.id"], name=op.f("fk_props_show_id_shows") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_props")), + ) + op.create_table( + "scenery", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("show_id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(), nullable=True), + sa.Column("description", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["show_id"], ["shows.id"], name=op.f("fk_scenery_show_id_shows") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_scenery")), + ) + op.create_table( + "props_allocation", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("props_id", sa.Integer(), nullable=False), + sa.Column("scene_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["props_id"], + ["props.id"], + name=op.f("fk_props_allocation_props_id_props"), + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + ["scene_id"], + ["scene.id"], + name=op.f("fk_props_allocation_scene_id_scene"), + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_props_allocation")), + ) + op.create_table( + "scenery_allocation", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("scenery_id", sa.Integer(), nullable=False), + sa.Column("scene_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["scene_id"], + ["scene.id"], + name=op.f("fk_scenery_allocation_scene_id_scene"), + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + ["scenery_id"], + ["scenery.id"], + name=op.f("fk_scenery_allocation_scenery_id_scenery"), + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_scenery_allocation")), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("scenery_allocation") + op.drop_table("props_allocation") + op.drop_table("scenery") + op.drop_table("props") + op.drop_table("crew") + # ### end Alembic commands ### diff --git a/server/controllers/api/show/stage/__init__.py b/server/controllers/api/show/stage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/controllers/api/show/stage/crew.py b/server/controllers/api/show/stage/crew.py new file mode 100644 index 00000000..168556c2 --- /dev/null +++ b/server/controllers/api/show/stage/crew.py @@ -0,0 +1,162 @@ +from tornado import escape + +from models.show import Show +from models.stage import Crew +from rbac.role import Role +from schemas.schemas import CrewSchema +from utils.web.base_controller import BaseAPIController +from utils.web.route import ApiRoute, ApiVersion +from utils.web.web_decorators import no_live_session, requires_show + + +@ApiRoute("show/stage/crew", ApiVersion.V1) +class CrewController(BaseAPIController): + @requires_show + def get(self): + current_show = self.get_current_show() + show_id = current_show["id"] + crew_schema = CrewSchema() + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + crew = [crew_schema.dump(c) for c in show.crew_list] + self.set_status(200) + self.finish({"crew": crew}) + else: + self.set_status(404) + self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def post(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + first_name = data.get("firstName", None) + if not first_name: + self.set_status(400) + await self.finish({"message": "First name missing"}) + return + + last_name = data.get("lastName", None) + if not last_name: + self.set_status(400) + await self.finish({"message": "Last name missing"}) + return + + new_crew = Crew( + show_id=show.id, first_name=first_name, last_name=last_name + ) + session.add(new_crew) + session.commit() + + self.set_status(200) + await self.finish( + {"id": new_crew.id, "message": "Successfully added crew member"} + ) + + await self.application.ws_send_to_all( + "GET_CREW_LIST", "GET_CREW_LIST", {} + ) + else: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def patch(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + crew_id = data.get("id", None) + if not crew_id: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + entry: Crew = session.get(Crew, crew_id) + if entry: + first_name = data.get("firstName", None) + if not first_name: + self.set_status(400) + await self.finish({"message": "First name missing"}) + return + entry.first_name = first_name + + last_name = data.get("lastName", None) + if not last_name: + self.set_status(400) + await self.finish({"message": "Last name missing"}) + return + entry.last_name = last_name + + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully updated crew member"}) + + await self.application.ws_send_to_all( + "GET_CREW_LIST", "GET_CREW_LIST", {} + ) + else: + self.set_status(404) + await self.finish({"message": "404 cast member not found"}) + return + else: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def delete(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + self.requires_role(show, Role.WRITE) + + crew_id_str = self.get_argument("id", None) + if not crew_id_str: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + try: + crew_id = int(crew_id_str) + except ValueError: + self.set_status(400) + await self.finish({"message": "Invalid ID"}) + return + + entry = session.get(Crew, crew_id) + if entry: + session.delete(entry) + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully deleted crew member"}) + + await self.application.ws_send_to_all( + "GET_CREW_LIST", "GET_CREW_LIST", {} + ) + else: + self.set_status(404) + await self.finish({"message": "404 crew member not found"}) + else: + self.set_status(404) + await self.finish({"message": "404 show not found"}) diff --git a/server/controllers/api/show/stage/props.py b/server/controllers/api/show/stage/props.py new file mode 100644 index 00000000..2234550c --- /dev/null +++ b/server/controllers/api/show/stage/props.py @@ -0,0 +1,504 @@ +from sqlalchemy import select +from tornado import escape + +from models.show import Scene, Show +from models.stage import Props, PropsAllocation, PropType +from rbac.role import Role +from schemas.schemas import PropsAllocationSchema, PropsSchema, PropTypeSchema +from utils.web.base_controller import BaseAPIController +from utils.web.route import ApiRoute, ApiVersion +from utils.web.web_decorators import no_live_session, requires_show + + +@ApiRoute("show/stage/props/types", ApiVersion.V1) +class PropsTypesController(BaseAPIController): + @requires_show + def get(self): + current_show = self.get_current_show() + show_id = current_show["id"] + prop_type_schema = PropTypeSchema() + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + prop_types = [prop_type_schema.dump(c) for c in show.prop_types] + self.set_status(200) + self.finish({"prop_types": prop_types}) + else: + self.set_status(404) + self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def post(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if not show: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + return + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + name = data.get("name", None) + if not name: + self.set_status(400) + await self.finish({"message": "Name missing"}) + return + + description = data.get("description", "") + + new_prop_type = PropType( + show_id=show.id, name=name, description=description + ) + session.add(new_prop_type) + session.commit() + + self.set_status(200) + await self.finish( + {"id": new_prop_type.id, "message": "Successfully added prop type"} + ) + + await self.application.ws_send_to_all("NOOP", "GET_PROP_TYPES", {}) + + @requires_show + @no_live_session + async def patch(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if not show: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + return + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + prop_type = data.get("id", None) + if not prop_type: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + entry: PropType = session.get(PropType, prop_type) + if not entry: + self.set_status(404) + await self.finish({"message": "404 prop type not found"}) + return + + name = data.get("name", None) + description = data.get("description", "") + if not name: + self.set_status(400) + await self.finish({"message": "Name missing"}) + return + + entry.name = name + entry.description = description + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully updated prop type"}) + + await self.application.ws_send_to_all("NOOP", "GET_PROP_TYPES", {}) + + @requires_show + @no_live_session + async def delete(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if not show: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + return + self.requires_role(show, Role.WRITE) + + prop_type_id_str = self.get_argument("id", None) + if not prop_type_id_str: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + try: + prop_type_id = int(prop_type_id_str) + except ValueError: + self.set_status(400) + await self.finish({"message": "Invalid ID"}) + return + + entry = session.get(PropType, prop_type_id) + if not entry: + self.set_status(404) + await self.finish({"message": "404 prop type not found"}) + return + + session.delete(entry) + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully deleted prop type"}) + + await self.application.ws_send_to_all("NOOP", "GET_PROP_TYPES", {}) + await self.application.ws_send_to_all("NOOP", "GET_PROPS_LIST", {}) + + +@ApiRoute("show/stage/props", ApiVersion.V1) +class PropsController(BaseAPIController): + @requires_show + def get(self): + current_show = self.get_current_show() + show_id = current_show["id"] + props_schema = PropsSchema() + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + props = [props_schema.dump(c) for c in show.props_list] + self.set_status(200) + self.finish({"props": props}) + else: + self.set_status(404) + self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def post(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + name = data.get("name", None) + if not name: + self.set_status(400) + await self.finish({"message": "Name missing"}) + return + + prop_type_id = data.get("prop_type_id", None) + if not prop_type_id: + self.set_status(400) + await self.finish({"message": "Prop type ID missing"}) + return + try: + prop_type_id = int(prop_type_id) + except ValueError: + self.set_status(400) + await self.finish({"message": "Invalid prop type ID"}) + return + prop_type: PropType = session.get(PropType, prop_type_id) + if not prop_type: + self.set_status(404) + await self.finish({"message": "Prop type not found"}) + return + if prop_type.show_id != show.id: + self.set_status(400) + await self.finish({"message": "Invalid prop type for show"}) + return + + description = data.get("description", "") + + new_props = Props( + show_id=show.id, + name=name, + description=description, + prop_type_id=prop_type.id, + ) + session.add(new_props) + session.commit() + + self.set_status(200) + await self.finish( + {"id": new_props.id, "message": "Successfully added props"} + ) + + await self.application.ws_send_to_all("NOOP", "GET_PROPS_LIST", {}) + else: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def patch(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + props = data.get("id", None) + if not props: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + entry: Props = session.get(Props, props) + if entry: + name = data.get("name", None) + if not name: + self.set_status(400) + await self.finish({"message": "Name missing"}) + return + entry.name = name + + prop_type_id = data.get("prop_type_id", None) + if not prop_type_id: + self.set_status(400) + await self.finish({"message": "Prop type ID missing"}) + return + try: + prop_type_id = int(prop_type_id) + except ValueError: + self.set_status(400) + await self.finish({"message": "Invalid prop type ID"}) + return + prop_type: PropType = session.get(PropType, prop_type_id) + if not prop_type: + self.set_status(404) + await self.finish({"message": "Prop type not found"}) + return + if prop_type.show_id != show.id: + self.set_status(400) + await self.finish({"message": "Invalid prop type for show"}) + return + entry.prop_type_id = prop_type.id + + description = data.get("description", "") + entry.description = description + + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully updated props"}) + + await self.application.ws_send_to_all("NOOP", "GET_PROPS_LIST", {}) + else: + self.set_status(404) + await self.finish({"message": "404 prop not found"}) + return + else: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def delete(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + self.requires_role(show, Role.WRITE) + + props_id_str = self.get_argument("id", None) + if not props_id_str: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + try: + props_id = int(props_id_str) + except ValueError: + self.set_status(400) + await self.finish({"message": "Invalid ID"}) + return + + entry = session.get(Props, props_id) + if entry: + session.delete(entry) + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully deleted props"}) + + await self.application.ws_send_to_all("NOOP", "GET_PROPS_LIST", {}) + else: + self.set_status(404) + await self.finish({"message": "404 props not found"}) + else: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + + +@ApiRoute("show/stage/props/allocations", ApiVersion.V1) +class PropsAllocationController(BaseAPIController): + """Controller for managing props allocations to scenes.""" + + @requires_show + def get(self): + """Get all props allocations for the current show.""" + current_show = self.get_current_show() + show_id = current_show["id"] + allocation_schema = PropsAllocationSchema() + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + allocations = session.scalars( + select(PropsAllocation) + .join(Props, PropsAllocation.props_id == Props.id) + .where(Props.show_id == show_id) + ).all() + allocations = [allocation_schema.dump(a) for a in allocations] + self.set_status(200) + self.finish({"allocations": allocations}) + else: + self.set_status(404) + self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def post(self): + """Create a new props allocation.""" + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if not show: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + return + + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + # Validate props_id + props_id = data.get("props_id", None) + if props_id is None: + self.set_status(400) + await self.finish({"message": "props_id missing"}) + return + + try: + props_id = int(props_id) + except ValueError: + self.set_status(400) + await self.finish({"message": "Invalid props_id"}) + return + + prop: Props = session.get(Props, props_id) + if not prop: + self.set_status(404) + await self.finish({"message": "404 prop not found"}) + return + + if prop.show_id != show_id: + self.set_status(404) + await self.finish({"message": "404 prop not found"}) + return + + # Validate scene_id + scene_id = data.get("scene_id", None) + if scene_id is None: + self.set_status(400) + await self.finish({"message": "scene_id missing"}) + return + + try: + scene_id = int(scene_id) + except ValueError: + self.set_status(400) + await self.finish({"message": "Invalid scene_id"}) + return + + scene: Scene = session.get(Scene, scene_id) + if not scene: + self.set_status(404) + await self.finish({"message": "404 scene not found"}) + return + + if scene.show_id != show_id: + self.set_status(404) + await self.finish({"message": "404 scene not found"}) + return + + # Check for duplicate allocation + existing = session.scalars( + select(PropsAllocation).where( + PropsAllocation.props_id == props_id, + PropsAllocation.scene_id == scene_id, + ) + ).first() + if existing: + self.set_status(400) + await self.finish( + {"message": "Allocation already exists for this prop and scene"} + ) + return + + new_allocation = PropsAllocation(props_id=props_id, scene_id=scene_id) + session.add(new_allocation) + session.commit() + + self.set_status(200) + await self.finish( + {"id": new_allocation.id, "message": "Successfully added allocation"} + ) + + await self.application.ws_send_to_all("NOOP", "GET_PROPS_ALLOCATIONS", {}) + + @requires_show + @no_live_session + async def delete(self): + """Delete a props allocation by ID (query parameter).""" + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if not show: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + return + + self.requires_role(show, Role.WRITE) + + allocation_id_str = self.get_argument("id", None) + if not allocation_id_str: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + try: + allocation_id = int(allocation_id_str) + except ValueError: + self.set_status(400) + await self.finish({"message": "Invalid ID"}) + return + + allocation: PropsAllocation = session.get(PropsAllocation, allocation_id) + if not allocation: + self.set_status(404) + await self.finish({"message": "404 allocation not found"}) + return + + # Verify the allocation belongs to a prop in this show + prop: Props = session.get(Props, allocation.props_id) + if not prop or prop.show_id != show_id: + self.set_status(404) + await self.finish({"message": "404 allocation not found"}) + return + + session.delete(allocation) + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully deleted allocation"}) + + await self.application.ws_send_to_all("NOOP", "GET_PROPS_ALLOCATIONS", {}) diff --git a/server/controllers/api/show/stage/scenery.py b/server/controllers/api/show/stage/scenery.py new file mode 100644 index 00000000..663945cf --- /dev/null +++ b/server/controllers/api/show/stage/scenery.py @@ -0,0 +1,517 @@ +from sqlalchemy import select +from tornado import escape + +from models.show import Scene, Show +from models.stage import Scenery, SceneryAllocation, SceneryType +from rbac.role import Role +from schemas.schemas import SceneryAllocationSchema, ScenerySchema, SceneryTypeSchema +from utils.web.base_controller import BaseAPIController +from utils.web.route import ApiRoute, ApiVersion +from utils.web.web_decorators import no_live_session, requires_show + + +@ApiRoute("show/stage/scenery/types", ApiVersion.V1) +class SceneryTypesController(BaseAPIController): + @requires_show + def get(self): + current_show = self.get_current_show() + show_id = current_show["id"] + scenery_type_schema = SceneryTypeSchema() + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + scenery_types = [ + scenery_type_schema.dump(c) for c in show.scenery_types + ] + self.set_status(200) + self.finish({"scenery_types": scenery_types}) + else: + self.set_status(404) + self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def post(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if not show: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + return + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + name = data.get("name", None) + if not name: + self.set_status(400) + await self.finish({"message": "Name missing"}) + return + + description = data.get("description", "") + + new_scenery_type = SceneryType( + show_id=show.id, name=name, description=description + ) + session.add(new_scenery_type) + session.commit() + + self.set_status(200) + await self.finish( + { + "id": new_scenery_type.id, + "message": "Successfully added scenery type", + } + ) + + await self.application.ws_send_to_all("NOOP", "GET_SCENERY_TYPES", {}) + + @requires_show + @no_live_session + async def patch(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if not show: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + return + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + prop_type = data.get("id", None) + if not prop_type: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + entry: SceneryType = session.get(SceneryType, prop_type) + if not entry: + self.set_status(404) + await self.finish({"message": "404 scenery type not found"}) + return + + name = data.get("name", None) + description = data.get("description", "") + if not name: + self.set_status(400) + await self.finish({"message": "Name missing"}) + return + + entry.name = name + entry.description = description + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully updated scenery type"}) + + await self.application.ws_send_to_all("NOOP", "GET_SCENERY_TYPES", {}) + + @requires_show + @no_live_session + async def delete(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if not show: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + return + self.requires_role(show, Role.WRITE) + + scenery_type_id_str = self.get_argument("id", None) + if not scenery_type_id_str: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + try: + scenery_type_id = int(scenery_type_id_str) + except ValueError: + self.set_status(400) + await self.finish({"message": "Invalid ID"}) + return + + entry = session.get(SceneryType, scenery_type_id) + if not entry: + self.set_status(404) + await self.finish({"message": "404 scenery type not found"}) + return + + session.delete(entry) + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully deleted scenery type"}) + + await self.application.ws_send_to_all("NOOP", "GET_SCENERY_TYPES", {}) + await self.application.ws_send_to_all("NOOP", "GET_SCENERY_LIST", {}) + + +@ApiRoute("show/stage/scenery", ApiVersion.V1) +class SceneryController(BaseAPIController): + @requires_show + def get(self): + current_show = self.get_current_show() + show_id = current_show["id"] + scenery_schema = ScenerySchema() + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + scenery = [scenery_schema.dump(c) for c in show.scenery_list] + self.set_status(200) + self.finish({"scenery": scenery}) + else: + self.set_status(404) + self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def post(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + name = data.get("name", None) + if not name: + self.set_status(400) + await self.finish({"message": "Name missing"}) + return + + scenery_type_id = data.get("scenery_type_id", None) + if not scenery_type_id: + self.set_status(400) + await self.finish({"message": "Scenery type ID missing"}) + return + try: + scenery_type_id = int(scenery_type_id) + except ValueError: + self.set_status(400) + await self.finish({"message": "Scenery prop type ID"}) + return + scenery_type: SceneryType = session.get(SceneryType, scenery_type_id) + if not scenery_type: + self.set_status(404) + await self.finish({"message": "Scenery type not found"}) + return + if scenery_type.show_id != show.id: + self.set_status(400) + await self.finish({"message": "Invalid scenery type for show"}) + return + + description = data.get("description", "") + + new_scenery = Scenery( + show_id=show.id, + name=name, + description=description, + scenery_type_id=scenery_type.id, + ) + session.add(new_scenery) + session.commit() + + self.set_status(200) + await self.finish( + {"id": new_scenery.id, "message": "Successfully added scenery"} + ) + + await self.application.ws_send_to_all("NOOP", "GET_SCENERY_LIST", {}) + else: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def patch(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + scenery = data.get("id", None) + if not scenery: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + entry: Scenery = session.get(Scenery, scenery) + if entry: + name = data.get("name", None) + if not name: + self.set_status(400) + await self.finish({"message": "Name missing"}) + return + entry.name = name + + scenery_type_id = data.get("scenery_type_id", None) + if not scenery_type_id: + self.set_status(400) + await self.finish({"message": "Scenery type ID missing"}) + return + try: + scenery_type_id = int(scenery_type_id) + except ValueError: + self.set_status(400) + await self.finish({"message": "Scenery prop type ID"}) + return + scenery_type: SceneryType = session.get( + SceneryType, scenery_type_id + ) + if not scenery_type: + self.set_status(404) + await self.finish({"message": "Scenery type not found"}) + return + if scenery_type.show_id != show.id: + self.set_status(400) + await self.finish({"message": "Invalid scenery type for show"}) + return + entry.scenery_type_id = scenery_type.id + + description = data.get("description", "") + entry.description = description + + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully updated scenery"}) + + await self.application.ws_send_to_all( + "NOOP", "GET_SCENERY_LIST", {} + ) + else: + self.set_status(404) + await self.finish({"message": "404 cast member not found"}) + return + else: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def delete(self): + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + self.requires_role(show, Role.WRITE) + + scenery_id_str = self.get_argument("id", None) + if not scenery_id_str: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + try: + scenery_id = int(scenery_id_str) + except ValueError: + self.set_status(400) + await self.finish({"message": "Invalid ID"}) + return + + entry = session.get(Scenery, scenery_id) + if entry: + session.delete(entry) + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully deleted scenery"}) + + await self.application.ws_send_to_all( + "NOOP", "GET_SCENERY_LIST", {} + ) + else: + self.set_status(404) + await self.finish({"message": "404 scenery not found"}) + else: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + + +@ApiRoute("show/stage/scenery/allocations", ApiVersion.V1) +class SceneryAllocationController(BaseAPIController): + """Controller for managing scenery allocations to scenes.""" + + @requires_show + def get(self): + """Get all scenery allocations for the current show.""" + current_show = self.get_current_show() + show_id = current_show["id"] + allocation_schema = SceneryAllocationSchema() + + with self.make_session() as session: + show = session.get(Show, show_id) + if show: + allocations = session.scalars( + select(SceneryAllocation) + .join(Scenery, SceneryAllocation.scenery_id == Scenery.id) + .where(Scenery.show_id == show_id) + ).all() + allocations = [allocation_schema.dump(a) for a in allocations] + self.set_status(200) + self.finish({"allocations": allocations}) + else: + self.set_status(404) + self.finish({"message": "404 show not found"}) + + @requires_show + @no_live_session + async def post(self): + """Create a new scenery allocation.""" + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if not show: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + return + + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + # Validate scenery_id + scenery_id = data.get("scenery_id", None) + if scenery_id is None: + self.set_status(400) + await self.finish({"message": "scenery_id missing"}) + return + + try: + scenery_id = int(scenery_id) + except ValueError: + self.set_status(400) + await self.finish({"message": "Invalid scenery_id"}) + return + + scenery: Scenery = session.get(Scenery, scenery_id) + if not scenery: + self.set_status(404) + await self.finish({"message": "404 scenery not found"}) + return + + if scenery.show_id != show_id: + self.set_status(404) + await self.finish({"message": "404 scenery not found"}) + return + + # Validate scene_id + scene_id = data.get("scene_id", None) + if scene_id is None: + self.set_status(400) + await self.finish({"message": "scene_id missing"}) + return + + try: + scene_id = int(scene_id) + except ValueError: + self.set_status(400) + await self.finish({"message": "Invalid scene_id"}) + return + + scene: Scene = session.get(Scene, scene_id) + if not scene: + self.set_status(404) + await self.finish({"message": "404 scene not found"}) + return + + if scene.show_id != show_id: + self.set_status(404) + await self.finish({"message": "404 scene not found"}) + return + + # Check for duplicate allocation + existing = session.scalars( + select(SceneryAllocation).where( + SceneryAllocation.scenery_id == scenery_id, + SceneryAllocation.scene_id == scene_id, + ) + ).first() + if existing: + self.set_status(400) + await self.finish( + {"message": "Allocation already exists for this scenery and scene"} + ) + return + + new_allocation = SceneryAllocation(scenery_id=scenery_id, scene_id=scene_id) + session.add(new_allocation) + session.commit() + + self.set_status(200) + await self.finish( + {"id": new_allocation.id, "message": "Successfully added allocation"} + ) + + await self.application.ws_send_to_all("NOOP", "GET_SCENERY_ALLOCATIONS", {}) + + @requires_show + @no_live_session + async def delete(self): + """Delete a scenery allocation by ID (query parameter).""" + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if not show: + self.set_status(404) + await self.finish({"message": "404 show not found"}) + return + + self.requires_role(show, Role.WRITE) + + allocation_id_str = self.get_argument("id", None) + if not allocation_id_str: + self.set_status(400) + await self.finish({"message": "ID missing"}) + return + + try: + allocation_id = int(allocation_id_str) + except ValueError: + self.set_status(400) + await self.finish({"message": "Invalid ID"}) + return + + allocation: SceneryAllocation = session.get( + SceneryAllocation, allocation_id + ) + if not allocation: + self.set_status(404) + await self.finish({"message": "404 allocation not found"}) + return + + # Verify the allocation belongs to scenery in this show + scenery: Scenery = session.get(Scenery, allocation.scenery_id) + if not scenery or scenery.show_id != show_id: + self.set_status(404) + await self.finish({"message": "404 allocation not found"}) + return + + session.delete(allocation) + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully deleted allocation"}) + + await self.application.ws_send_to_all("NOOP", "GET_SCENERY_ALLOCATIONS", {}) diff --git a/server/models/show.py b/server/models/show.py index ce739b63..7ba93f54 100644 --- a/server/models/show.py +++ b/server/models/show.py @@ -15,6 +15,15 @@ from models.mics import MicrophoneAllocation from models.script import ScriptLine from models.session import ShowSession + from models.stage import ( + Crew, + Props, + PropsAllocation, + PropType, + Scenery, + SceneryAllocation, + SceneryType, + ) class ShowScriptType(enum.IntEnum): @@ -66,8 +75,27 @@ class Show(db.Model): current_session: Mapped[ShowSession] = relationship( foreign_keys=[current_session_id] ) - cast_list: Mapped[List[Cast]] = relationship(cascade="all, delete-orphan") + crew_list: Mapped[List[Crew]] = relationship( + back_populates="show", + cascade="all, delete-orphan", + ) + scenery_types: Mapped[List[SceneryType]] = relationship( + back_populates="show", + cascade="all, delete-orphan", + ) + scenery_list: Mapped[List[Scenery]] = relationship( + back_populates="show", + cascade="all, delete-orphan", + ) + prop_types: Mapped[List[PropType]] = relationship( + back_populates="show", + cascade="all, delete-orphan", + ) + props_list: Mapped[List[Props]] = relationship( + back_populates="show", + cascade="all, delete-orphan", + ) character_list: Mapped[List[Character]] = relationship(cascade="all, delete-orphan") character_group_list: Mapped[List[CharacterGroup]] = relationship( cascade="all, delete-orphan" @@ -192,3 +220,11 @@ class Scene(db.Model): mic_allocations: Mapped[List[MicrophoneAllocation]] = relationship( cascade="all, delete-orphan", back_populates="scene" ) + scenery_allocations: Mapped[List["SceneryAllocation"]] = relationship( + back_populates="scene", + cascade="all, delete-orphan", + ) + props_allocations: Mapped[List["PropsAllocation"]] = relationship( + back_populates="scene", + cascade="all, delete-orphan", + ) diff --git a/server/models/stage.py b/server/models/stage.py new file mode 100644 index 00000000..795aefd3 --- /dev/null +++ b/server/models/stage.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +from typing import List + +from sqlalchemy import ForeignKey, UniqueConstraint +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from models.models import db +from models.show import Scene, Show + + +class Crew(db.Model): + __tablename__ = "crew" + + id: Mapped[int] = mapped_column(primary_key=True) + show_id: Mapped[int] = mapped_column(ForeignKey("shows.id")) + first_name: Mapped[str] = mapped_column() + last_name: Mapped[str | None] = mapped_column() + + show: Mapped[Show] = relationship(back_populates="crew_list") + + +class SceneryAllocation(db.Model): + __tablename__ = "scenery_allocation" + __table_args__ = ( + UniqueConstraint("scenery_id", "scene_id", name="uq_scenery_scene"), + ) + + id: Mapped[int] = mapped_column(primary_key=True) + scenery_id: Mapped[int] = mapped_column( + ForeignKey("scenery.id", ondelete="CASCADE") + ) + scene_id: Mapped[int] = mapped_column(ForeignKey("scene.id", ondelete="CASCADE")) + + scenery: Mapped[Scenery] = relationship( + back_populates="scene_allocations", + foreign_keys=[scenery_id], + ) + scene: Mapped[Scene] = relationship( + back_populates="scenery_allocations", + foreign_keys=[scene_id], + ) + + +class PropsAllocation(db.Model): + __tablename__ = "props_allocation" + __table_args__ = (UniqueConstraint("props_id", "scene_id", name="uq_props_scene"),) + + id: Mapped[int] = mapped_column(primary_key=True) + props_id: Mapped[int] = mapped_column(ForeignKey("props.id", ondelete="CASCADE")) + scene_id: Mapped[int] = mapped_column(ForeignKey("scene.id", ondelete="CASCADE")) + + prop: Mapped[Props] = relationship( + back_populates="scene_allocations", + foreign_keys=[props_id], + ) + scene: Mapped[Scene] = relationship( + back_populates="props_allocations", + foreign_keys=[scene_id], + ) + + +class SceneryType(db.Model): + __tablename__ = "scenery_type" + + id: Mapped[int] = mapped_column(primary_key=True) + show_id: Mapped[int] = mapped_column(ForeignKey("shows.id")) + name: Mapped[str] = mapped_column() + description: Mapped[str | None] = mapped_column() + + show: Mapped[Show] = relationship(back_populates="scenery_types") + scenery_items: Mapped[list[Scenery]] = relationship( + back_populates="scenery_type", + cascade="all, delete-orphan", + ) + + +class Scenery(db.Model): + __tablename__ = "scenery" + + id: Mapped[int] = mapped_column(primary_key=True) + show_id: Mapped[int] = mapped_column(ForeignKey("shows.id")) + scenery_type_id: Mapped[int] = mapped_column(ForeignKey("scenery_type.id")) + name: Mapped[str] = mapped_column() + description: Mapped[str | None] = mapped_column() + + show: Mapped[Show] = relationship(back_populates="scenery_list") + scenery_type: Mapped[SceneryType] = relationship(back_populates="scenery_items") + scene_allocations: Mapped[List[SceneryAllocation]] = relationship( + back_populates="scenery", + cascade="all, delete-orphan", + ) + + +class PropType(db.Model): + __tablename__ = "prop_type" + + id: Mapped[int] = mapped_column(primary_key=True) + show_id: Mapped[int] = mapped_column(ForeignKey("shows.id")) + name: Mapped[str] = mapped_column() + description: Mapped[str | None] = mapped_column() + + show: Mapped[Show] = relationship(back_populates="prop_types") + prop_items: Mapped[List[Props]] = relationship( + back_populates="prop_type", + cascade="all, delete-orphan", + ) + + +class Props(db.Model): + __tablename__ = "props" + + id: Mapped[int] = mapped_column(primary_key=True) + show_id: Mapped[int] = mapped_column(ForeignKey("shows.id")) + prop_type_id: Mapped[int] = mapped_column(ForeignKey("prop_type.id")) + name: Mapped[str] = mapped_column() + description: Mapped[str | None] = mapped_column() + + show: Mapped[Show] = relationship(back_populates="props_list") + prop_type: Mapped[PropType] = relationship(back_populates="prop_items") + scene_allocations: Mapped[list[PropsAllocation]] = relationship( + back_populates="prop", + cascade="all, delete-orphan", + ) diff --git a/server/schemas/schemas.py b/server/schemas/schemas.py index 84b4a383..d6026206 100644 --- a/server/schemas/schemas.py +++ b/server/schemas/schemas.py @@ -14,6 +14,15 @@ ) from models.session import Interval, Session, SessionTag, ShowSession from models.show import Act, Cast, Character, CharacterGroup, Scene, Show +from models.stage import ( + Crew, + Props, + PropsAllocation, + PropType, + Scenery, + SceneryAllocation, + SceneryType, +) from models.user import User, UserSettings from registry.schema import get_registry @@ -72,6 +81,62 @@ class Meta: ) +@schema +class CrewSchema(SQLAlchemyAutoSchema): + class Meta: + model = Crew + load_instance = True + include_fk = True + + +@schema +class SceneryTypeSchema(SQLAlchemyAutoSchema): + class Meta: + model = SceneryType + load_instance = True + include_fk = True + + +@schema +class ScenerySchema(SQLAlchemyAutoSchema): + class Meta: + model = Scenery + load_instance = True + include_fk = True + + +@schema +class PropsSchema(SQLAlchemyAutoSchema): + class Meta: + model = Props + load_instance = True + include_fk = True + + +@schema +class PropTypeSchema(SQLAlchemyAutoSchema): + class Meta: + model = PropType + load_instance = True + include_fk = True + + +@schema +class PropsAllocationSchema(SQLAlchemyAutoSchema): + class Meta: + model = PropsAllocation + load_instance = True + include_fk = True + + +@schema +class SceneryAllocationSchema(SQLAlchemyAutoSchema): + class Meta: + model = SceneryAllocation + load_instance = True + include_fk = True + + @schema class CharacterSchema(SQLAlchemyAutoSchema): class Meta: diff --git a/server/test/controllers/api/show/stage/__init__.py b/server/test/controllers/api/show/stage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/test/controllers/api/show/stage/test_crew.py b/server/test/controllers/api/show/stage/test_crew.py new file mode 100644 index 00000000..49550867 --- /dev/null +++ b/server/test/controllers/api/show/stage/test_crew.py @@ -0,0 +1,288 @@ +import tornado.escape + +from models.show import Show, ShowScriptType +from models.stage import Crew +from models.user import User +from test.conftest import DigiScriptTestCase + + +class TestCrewController(DigiScriptTestCase): + """Test suite for /api/v1/show/stage/crew endpoint.""" + + def setUp(self): + super().setUp() + with self._app.get_db().sessionmaker() as session: + show = Show(name="Test Show", script_mode=ShowScriptType.FULL) + session.add(show) + session.flush() + self.show_id = show.id + + # Create admin user for RBAC + admin = User(username="admin", is_admin=True, password="test") + session.add(admin) + session.flush() + self.user_id = admin.id + + session.commit() + + self._app.digi_settings.settings["current_show"].set_value(self.show_id) + self.token = self._app.jwt_service.create_access_token( + data={"user_id": self.user_id} + ) + + # GET tests + + def test_get_crew_empty(self): + """Test GET with no crew returns empty list.""" + response = self.fetch("/api/v1/show/stage/crew") + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("crew", response_body) + self.assertEqual([], response_body["crew"]) + + def test_get_crew_returns_all(self): + """Test GET returns all crew members for the show.""" + with self._app.get_db().sessionmaker() as session: + crew_member = Crew(show_id=self.show_id, first_name="John", last_name="Doe") + session.add(crew_member) + session.commit() + + response = self.fetch("/api/v1/show/stage/crew") + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertEqual(1, len(response_body["crew"])) + self.assertEqual("John", response_body["crew"][0]["first_name"]) + self.assertEqual("Doe", response_body["crew"][0]["last_name"]) + + def test_get_crew_no_show(self): + """Test GET returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch("/api/v1/show/stage/crew") + self.assertEqual(400, response.code) + + # POST tests + + def test_create_crew_success(self): + """Test POST creates a new crew member.""" + response = self.fetch( + "/api/v1/show/stage/crew", + method="POST", + body=tornado.escape.json_encode({"firstName": "Jane", "lastName": "Smith"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("id", response_body) + self.assertIn("message", response_body) + + # Verify crew member was created + with self._app.get_db().sessionmaker() as session: + crew = session.get(Crew, response_body["id"]) + self.assertIsNotNone(crew) + self.assertEqual("Jane", crew.first_name) + self.assertEqual("Smith", crew.last_name) + + def test_create_crew_missing_first_name(self): + """Test POST returns 400 when firstName is missing.""" + response = self.fetch( + "/api/v1/show/stage/crew", + method="POST", + body=tornado.escape.json_encode({"lastName": "Smith"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("First name missing", response_body["message"]) + + def test_create_crew_missing_last_name(self): + """Test POST returns 400 when lastName is missing.""" + response = self.fetch( + "/api/v1/show/stage/crew", + method="POST", + body=tornado.escape.json_encode({"firstName": "Jane"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Last name missing", response_body["message"]) + + def test_create_crew_no_show(self): + """Test POST returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch( + "/api/v1/show/stage/crew", + method="POST", + body=tornado.escape.json_encode({"firstName": "Jane", "lastName": "Smith"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + + # PATCH tests + + def test_update_crew_success(self): + """Test PATCH updates an existing crew member.""" + # Create a crew member first + with self._app.get_db().sessionmaker() as session: + crew_member = Crew(show_id=self.show_id, first_name="John", last_name="Doe") + session.add(crew_member) + session.flush() + crew_id = crew_member.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/crew", + method="PATCH", + body=tornado.escape.json_encode( + {"id": crew_id, "firstName": "Jane", "lastName": "Smith"} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + + # Verify update + with self._app.get_db().sessionmaker() as session: + crew = session.get(Crew, crew_id) + self.assertEqual("Jane", crew.first_name) + self.assertEqual("Smith", crew.last_name) + + def test_update_crew_missing_id(self): + """Test PATCH returns 400 when id is missing.""" + response = self.fetch( + "/api/v1/show/stage/crew", + method="PATCH", + body=tornado.escape.json_encode({"firstName": "Jane", "lastName": "Smith"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("ID missing", response_body["message"]) + + def test_update_crew_not_found(self): + """Test PATCH returns 404 for non-existent crew member.""" + response = self.fetch( + "/api/v1/show/stage/crew", + method="PATCH", + body=tornado.escape.json_encode( + {"id": 99999, "firstName": "Jane", "lastName": "Smith"} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(404, response.code) + + def test_update_crew_missing_first_name(self): + """Test PATCH returns 400 when firstName is missing.""" + # Create a crew member first + with self._app.get_db().sessionmaker() as session: + crew_member = Crew(show_id=self.show_id, first_name="John", last_name="Doe") + session.add(crew_member) + session.flush() + crew_id = crew_member.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/crew", + method="PATCH", + body=tornado.escape.json_encode({"id": crew_id, "lastName": "Smith"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("First name missing", response_body["message"]) + + def test_update_crew_missing_last_name(self): + """Test PATCH returns 400 when lastName is missing.""" + # Create a crew member first + with self._app.get_db().sessionmaker() as session: + crew_member = Crew(show_id=self.show_id, first_name="John", last_name="Doe") + session.add(crew_member) + session.flush() + crew_id = crew_member.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/crew", + method="PATCH", + body=tornado.escape.json_encode({"id": crew_id, "firstName": "Jane"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Last name missing", response_body["message"]) + + def test_update_crew_no_show(self): + """Test PATCH returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch( + "/api/v1/show/stage/crew", + method="PATCH", + body=tornado.escape.json_encode( + {"id": 1, "firstName": "Jane", "lastName": "Smith"} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + + # DELETE tests + + def test_delete_crew_success(self): + """Test DELETE removes a crew member.""" + # Create a crew member first + with self._app.get_db().sessionmaker() as session: + crew_member = Crew(show_id=self.show_id, first_name="John", last_name="Doe") + session.add(crew_member) + session.flush() + crew_id = crew_member.id + session.commit() + + response = self.fetch( + f"/api/v1/show/stage/crew?id={crew_id}", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + + # Verify deletion + with self._app.get_db().sessionmaker() as session: + crew = session.get(Crew, crew_id) + self.assertIsNone(crew) + + def test_delete_crew_missing_id(self): + """Test DELETE returns 400 when id is missing.""" + response = self.fetch( + "/api/v1/show/stage/crew", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("ID missing", response_body["message"]) + + def test_delete_crew_not_found(self): + """Test DELETE returns 404 for non-existent crew member.""" + response = self.fetch( + "/api/v1/show/stage/crew?id=99999", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(404, response.code) + + def test_delete_crew_invalid_id(self): + """Test DELETE returns 400 when id is not a valid integer.""" + response = self.fetch( + "/api/v1/show/stage/crew?id=not-a-number", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Invalid ID", response_body["message"]) + + def test_delete_crew_no_show(self): + """Test DELETE returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch( + "/api/v1/show/stage/crew?id=1", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) diff --git a/server/test/controllers/api/show/stage/test_props.py b/server/test/controllers/api/show/stage/test_props.py new file mode 100644 index 00000000..135c9198 --- /dev/null +++ b/server/test/controllers/api/show/stage/test_props.py @@ -0,0 +1,1058 @@ +import tornado.escape + +from models.show import Act, Scene, Show, ShowScriptType +from models.stage import Props, PropsAllocation, PropType +from models.user import User +from test.conftest import DigiScriptTestCase + + +class TestPropsAllocationController(DigiScriptTestCase): + """Test suite for /api/v1/show/stage/props/allocations endpoint.""" + + def setUp(self): + super().setUp() + with self._app.get_db().sessionmaker() as session: + show = Show(name="Test Show", script_mode=ShowScriptType.FULL) + session.add(show) + session.flush() + self.show_id = show.id + + # Create an act and scene + act = Act(show_id=show.id, name="Act 1", interval_after=False) + session.add(act) + session.flush() + self.act_id = act.id + + scene = Scene( + show_id=show.id, + act_id=act.id, + name="Scene 1", + previous_scene_id=None, + ) + session.add(scene) + session.flush() + self.scene_id = scene.id + + # Create a prop type and prop + prop_type = PropType(show_id=show.id, name="Hand Props", description="") + session.add(prop_type) + session.flush() + self.prop_type_id = prop_type.id + + prop = Props( + show_id=show.id, + prop_type_id=prop_type.id, + name="Sword", + description="Test prop", + ) + session.add(prop) + session.flush() + self.prop_id = prop.id + + # Create admin user for RBAC + admin = User(username="admin", is_admin=True, password="test") + session.add(admin) + session.flush() + self.user_id = admin.id + + session.commit() + + self._app.digi_settings.settings["current_show"].set_value(self.show_id) + self.token = self._app.jwt_service.create_access_token( + data={"user_id": self.user_id} + ) + + def test_get_allocations_empty(self): + """Test GET with no allocations returns empty list.""" + response = self.fetch("/api/v1/show/stage/props/allocations") + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("allocations", response_body) + self.assertEqual([], response_body["allocations"]) + + def test_get_allocations_returns_all(self): + """Test GET returns all allocations for the show.""" + # Create an allocation + with self._app.get_db().sessionmaker() as session: + allocation = PropsAllocation(props_id=self.prop_id, scene_id=self.scene_id) + session.add(allocation) + session.commit() + + response = self.fetch("/api/v1/show/stage/props/allocations") + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertEqual(1, len(response_body["allocations"])) + self.assertEqual(self.prop_id, response_body["allocations"][0]["props_id"]) + self.assertEqual(self.scene_id, response_body["allocations"][0]["scene_id"]) + + def test_get_allocations_no_show(self): + """Test GET returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch("/api/v1/show/stage/props/allocations") + self.assertEqual(400, response.code) + + def test_create_allocation_success(self): + """Test POST creates a new allocation.""" + response = self.fetch( + "/api/v1/show/stage/props/allocations", + method="POST", + body=tornado.escape.json_encode( + {"props_id": self.prop_id, "scene_id": self.scene_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("id", response_body) + self.assertIn("message", response_body) + + # Verify allocation was created + with self._app.get_db().sessionmaker() as session: + allocation = session.get(PropsAllocation, response_body["id"]) + self.assertIsNotNone(allocation) + self.assertEqual(self.prop_id, allocation.props_id) + self.assertEqual(self.scene_id, allocation.scene_id) + + def test_create_allocation_duplicate(self): + """Test POST returns 400 for duplicate allocation.""" + # Create initial allocation + with self._app.get_db().sessionmaker() as session: + allocation = PropsAllocation(props_id=self.prop_id, scene_id=self.scene_id) + session.add(allocation) + session.commit() + + # Try to create duplicate + response = self.fetch( + "/api/v1/show/stage/props/allocations", + method="POST", + body=tornado.escape.json_encode( + {"props_id": self.prop_id, "scene_id": self.scene_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("already exists", response_body["message"]) + + def test_create_allocation_invalid_props_id(self): + """Test POST returns 404 for non-existent prop.""" + response = self.fetch( + "/api/v1/show/stage/props/allocations", + method="POST", + body=tornado.escape.json_encode( + {"props_id": 99999, "scene_id": self.scene_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(404, response.code) + + def test_create_allocation_invalid_scene_id(self): + """Test POST returns 404 for non-existent scene.""" + response = self.fetch( + "/api/v1/show/stage/props/allocations", + method="POST", + body=tornado.escape.json_encode( + {"props_id": self.prop_id, "scene_id": 99999} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(404, response.code) + + def test_create_allocation_prop_wrong_show(self): + """Test POST returns 404 for prop from different show.""" + # Create another show with a prop + with self._app.get_db().sessionmaker() as session: + other_show = Show(name="Other Show", script_mode=ShowScriptType.FULL) + session.add(other_show) + session.flush() + + other_prop_type = PropType( + show_id=other_show.id, name="Other Type", description="" + ) + session.add(other_prop_type) + session.flush() + + other_prop = Props( + show_id=other_show.id, + prop_type_id=other_prop_type.id, + name="Other Prop", + description="", + ) + session.add(other_prop) + session.flush() + other_prop_id = other_prop.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/props/allocations", + method="POST", + body=tornado.escape.json_encode( + {"props_id": other_prop_id, "scene_id": self.scene_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(404, response.code) + + def test_create_allocation_missing_props_id(self): + """Test POST returns 400 when props_id is missing.""" + response = self.fetch( + "/api/v1/show/stage/props/allocations", + method="POST", + body=tornado.escape.json_encode({"scene_id": self.scene_id}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("props_id missing", response_body["message"]) + + def test_create_allocation_missing_scene_id(self): + """Test POST returns 400 when scene_id is missing.""" + response = self.fetch( + "/api/v1/show/stage/props/allocations", + method="POST", + body=tornado.escape.json_encode({"props_id": self.prop_id}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("scene_id missing", response_body["message"]) + + def test_create_allocation_no_show(self): + """Test POST returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch( + "/api/v1/show/stage/props/allocations", + method="POST", + body=tornado.escape.json_encode( + {"props_id": self.prop_id, "scene_id": self.scene_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + + def test_delete_allocation_success(self): + """Test DELETE removes an allocation.""" + # Create an allocation + with self._app.get_db().sessionmaker() as session: + allocation = PropsAllocation(props_id=self.prop_id, scene_id=self.scene_id) + session.add(allocation) + session.flush() + allocation_id = allocation.id + session.commit() + + response = self.fetch( + f"/api/v1/show/stage/props/allocations?id={allocation_id}", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + + # Verify allocation was deleted + with self._app.get_db().sessionmaker() as session: + allocation = session.get(PropsAllocation, allocation_id) + self.assertIsNone(allocation) + + def test_delete_allocation_not_found(self): + """Test DELETE returns 404 for non-existent allocation.""" + response = self.fetch( + "/api/v1/show/stage/props/allocations?id=99999", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(404, response.code) + + def test_delete_allocation_missing_id(self): + """Test DELETE returns 400 when ID is missing.""" + response = self.fetch( + "/api/v1/show/stage/props/allocations", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("ID missing", response_body["message"]) + + def test_delete_allocation_invalid_id(self): + """Test DELETE returns 400 for non-integer ID.""" + response = self.fetch( + "/api/v1/show/stage/props/allocations?id=invalid", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Invalid ID", response_body["message"]) + + def test_delete_allocation_no_show(self): + """Test DELETE returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch( + "/api/v1/show/stage/props/allocations?id=1", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + + +class TestPropTypesController(DigiScriptTestCase): + """Test suite for /api/v1/show/stage/props/types endpoint.""" + + def setUp(self): + super().setUp() + with self._app.get_db().sessionmaker() as session: + show = Show(name="Test Show", script_mode=ShowScriptType.FULL) + session.add(show) + session.flush() + self.show_id = show.id + + # Create admin user for RBAC + admin = User(username="admin", is_admin=True, password="test") + session.add(admin) + session.flush() + self.user_id = admin.id + + session.commit() + + self._app.digi_settings.settings["current_show"].set_value(self.show_id) + self.token = self._app.jwt_service.create_access_token( + data={"user_id": self.user_id} + ) + + # GET tests + + def test_get_prop_types_empty(self): + """Test GET with no prop types returns empty list.""" + response = self.fetch("/api/v1/show/stage/props/types") + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("prop_types", response_body) + self.assertEqual([], response_body["prop_types"]) + + def test_get_prop_types_returns_all(self): + """Test GET returns all prop types for the show.""" + with self._app.get_db().sessionmaker() as session: + prop_type = PropType( + show_id=self.show_id, name="Hand Props", description="Small items" + ) + session.add(prop_type) + session.commit() + + response = self.fetch("/api/v1/show/stage/props/types") + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertEqual(1, len(response_body["prop_types"])) + self.assertEqual("Hand Props", response_body["prop_types"][0]["name"]) + + def test_get_prop_types_no_show(self): + """Test GET returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch("/api/v1/show/stage/props/types") + self.assertEqual(400, response.code) + + # POST tests + + def test_create_prop_type_success(self): + """Test POST creates a new prop type.""" + response = self.fetch( + "/api/v1/show/stage/props/types", + method="POST", + body=tornado.escape.json_encode({"name": "Furniture"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("id", response_body) + self.assertIn("message", response_body) + + # Verify prop type was created + with self._app.get_db().sessionmaker() as session: + prop_type = session.get(PropType, response_body["id"]) + self.assertIsNotNone(prop_type) + self.assertEqual("Furniture", prop_type.name) + + def test_create_prop_type_with_description(self): + """Test POST creates a prop type with description.""" + response = self.fetch( + "/api/v1/show/stage/props/types", + method="POST", + body=tornado.escape.json_encode( + {"name": "Furniture", "description": "Tables and chairs"} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + + # Verify description was saved + with self._app.get_db().sessionmaker() as session: + prop_type = session.get(PropType, response_body["id"]) + self.assertEqual("Tables and chairs", prop_type.description) + + def test_create_prop_type_missing_name(self): + """Test POST returns 400 when name is missing.""" + response = self.fetch( + "/api/v1/show/stage/props/types", + method="POST", + body=tornado.escape.json_encode({"description": "Some description"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Name missing", response_body["message"]) + + def test_create_prop_type_no_show(self): + """Test POST returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch( + "/api/v1/show/stage/props/types", + method="POST", + body=tornado.escape.json_encode({"name": "Furniture"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + + # PATCH tests + + def test_update_prop_type_success(self): + """Test PATCH updates an existing prop type.""" + # Create a prop type first + with self._app.get_db().sessionmaker() as session: + prop_type = PropType( + show_id=self.show_id, name="Hand Props", description="" + ) + session.add(prop_type) + session.flush() + prop_type_id = prop_type.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/props/types", + method="PATCH", + body=tornado.escape.json_encode( + {"id": prop_type_id, "name": "Stage Props", "description": "Updated"} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + + # Verify update + with self._app.get_db().sessionmaker() as session: + prop_type = session.get(PropType, prop_type_id) + self.assertEqual("Stage Props", prop_type.name) + self.assertEqual("Updated", prop_type.description) + + def test_update_prop_type_missing_id(self): + """Test PATCH returns 400 when id is missing.""" + response = self.fetch( + "/api/v1/show/stage/props/types", + method="PATCH", + body=tornado.escape.json_encode({"name": "Stage Props"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("ID missing", response_body["message"]) + + def test_update_prop_type_not_found(self): + """Test PATCH returns 404 for non-existent prop type.""" + response = self.fetch( + "/api/v1/show/stage/props/types", + method="PATCH", + body=tornado.escape.json_encode({"id": 99999, "name": "Stage Props"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(404, response.code) + + def test_update_prop_type_missing_name(self): + """Test PATCH returns 400 when name is missing.""" + # Create a prop type first + with self._app.get_db().sessionmaker() as session: + prop_type = PropType( + show_id=self.show_id, name="Hand Props", description="" + ) + session.add(prop_type) + session.flush() + prop_type_id = prop_type.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/props/types", + method="PATCH", + body=tornado.escape.json_encode({"id": prop_type_id}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Name missing", response_body["message"]) + + def test_update_prop_type_no_show(self): + """Test PATCH returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch( + "/api/v1/show/stage/props/types", + method="PATCH", + body=tornado.escape.json_encode({"id": 1, "name": "Stage Props"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + + # DELETE tests + + def test_delete_prop_type_success(self): + """Test DELETE removes a prop type.""" + # Create a prop type first + with self._app.get_db().sessionmaker() as session: + prop_type = PropType( + show_id=self.show_id, name="Hand Props", description="" + ) + session.add(prop_type) + session.flush() + prop_type_id = prop_type.id + session.commit() + + response = self.fetch( + f"/api/v1/show/stage/props/types?id={prop_type_id}", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + + # Verify deletion + with self._app.get_db().sessionmaker() as session: + prop_type = session.get(PropType, prop_type_id) + self.assertIsNone(prop_type) + + def test_delete_prop_type_missing_id(self): + """Test DELETE returns 400 when id is missing.""" + response = self.fetch( + "/api/v1/show/stage/props/types", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("ID missing", response_body["message"]) + + def test_delete_prop_type_invalid_id(self): + """Test DELETE returns 400 for non-integer ID.""" + response = self.fetch( + "/api/v1/show/stage/props/types?id=invalid", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Invalid ID", response_body["message"]) + + def test_delete_prop_type_not_found(self): + """Test DELETE returns 404 for non-existent prop type.""" + response = self.fetch( + "/api/v1/show/stage/props/types?id=99999", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(404, response.code) + + def test_delete_prop_type_no_show(self): + """Test DELETE returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch( + "/api/v1/show/stage/props/types?id=1", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + + +class TestPropsController(DigiScriptTestCase): + """Test suite for /api/v1/show/stage/props endpoint.""" + + def setUp(self): + super().setUp() + with self._app.get_db().sessionmaker() as session: + show = Show(name="Test Show", script_mode=ShowScriptType.FULL) + session.add(show) + session.flush() + self.show_id = show.id + + # Create a prop type + prop_type = PropType(show_id=show.id, name="Hand Props", description="") + session.add(prop_type) + session.flush() + self.prop_type_id = prop_type.id + + # Create admin user for RBAC + admin = User(username="admin", is_admin=True, password="test") + session.add(admin) + session.flush() + self.user_id = admin.id + + session.commit() + + self._app.digi_settings.settings["current_show"].set_value(self.show_id) + self.token = self._app.jwt_service.create_access_token( + data={"user_id": self.user_id} + ) + + # GET tests + + def test_get_props_empty(self): + """Test GET with no props returns empty list.""" + response = self.fetch("/api/v1/show/stage/props") + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("props", response_body) + self.assertEqual([], response_body["props"]) + + def test_get_props_returns_all(self): + """Test GET returns all props for the show.""" + with self._app.get_db().sessionmaker() as session: + prop = Props( + show_id=self.show_id, + prop_type_id=self.prop_type_id, + name="Sword", + description="A prop sword", + ) + session.add(prop) + session.commit() + + response = self.fetch("/api/v1/show/stage/props") + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertEqual(1, len(response_body["props"])) + self.assertEqual("Sword", response_body["props"][0]["name"]) + + def test_get_props_no_show(self): + """Test GET returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch("/api/v1/show/stage/props") + self.assertEqual(400, response.code) + + # POST tests + + def test_create_props_success(self): + """Test POST creates a new prop.""" + response = self.fetch( + "/api/v1/show/stage/props", + method="POST", + body=tornado.escape.json_encode( + {"name": "Sword", "prop_type_id": self.prop_type_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("id", response_body) + self.assertIn("message", response_body) + + # Verify prop was created + with self._app.get_db().sessionmaker() as session: + prop = session.get(Props, response_body["id"]) + self.assertIsNotNone(prop) + self.assertEqual("Sword", prop.name) + self.assertEqual(self.prop_type_id, prop.prop_type_id) + + def test_create_props_with_description(self): + """Test POST creates a prop with description.""" + response = self.fetch( + "/api/v1/show/stage/props", + method="POST", + body=tornado.escape.json_encode( + { + "name": "Sword", + "prop_type_id": self.prop_type_id, + "description": "A medieval sword", + } + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + + # Verify description was saved + with self._app.get_db().sessionmaker() as session: + prop = session.get(Props, response_body["id"]) + self.assertEqual("A medieval sword", prop.description) + + def test_create_props_missing_name(self): + """Test POST returns 400 when name is missing.""" + response = self.fetch( + "/api/v1/show/stage/props", + method="POST", + body=tornado.escape.json_encode({"prop_type_id": self.prop_type_id}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Name missing", response_body["message"]) + + def test_create_props_missing_prop_type_id(self): + """Test POST returns 400 when prop_type_id is missing.""" + response = self.fetch( + "/api/v1/show/stage/props", + method="POST", + body=tornado.escape.json_encode({"name": "Sword"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Prop type ID missing", response_body["message"]) + + def test_create_props_invalid_prop_type_id(self): + """Test POST returns 400 for non-integer prop_type_id.""" + response = self.fetch( + "/api/v1/show/stage/props", + method="POST", + body=tornado.escape.json_encode( + {"name": "Sword", "prop_type_id": "invalid"} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Invalid prop type ID", response_body["message"]) + + def test_create_props_prop_type_not_found(self): + """Test POST returns 404 for non-existent prop type.""" + response = self.fetch( + "/api/v1/show/stage/props", + method="POST", + body=tornado.escape.json_encode({"name": "Sword", "prop_type_id": 99999}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(404, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Prop type not found", response_body["message"]) + + def test_create_props_prop_type_wrong_show(self): + """Test POST returns 400 for prop type from different show.""" + # Create another show with a prop type + with self._app.get_db().sessionmaker() as session: + other_show = Show(name="Other Show", script_mode=ShowScriptType.FULL) + session.add(other_show) + session.flush() + + other_prop_type = PropType( + show_id=other_show.id, name="Other Type", description="" + ) + session.add(other_prop_type) + session.flush() + other_prop_type_id = other_prop_type.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/props", + method="POST", + body=tornado.escape.json_encode( + {"name": "Sword", "prop_type_id": other_prop_type_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Invalid prop type for show", response_body["message"]) + + def test_create_props_no_show(self): + """Test POST returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch( + "/api/v1/show/stage/props", + method="POST", + body=tornado.escape.json_encode( + {"name": "Sword", "prop_type_id": self.prop_type_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + + # PATCH tests + + def test_update_props_success(self): + """Test PATCH updates an existing prop.""" + # Create a prop first + with self._app.get_db().sessionmaker() as session: + prop = Props( + show_id=self.show_id, + prop_type_id=self.prop_type_id, + name="Sword", + description="", + ) + session.add(prop) + session.flush() + prop_id = prop.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/props", + method="PATCH", + body=tornado.escape.json_encode( + { + "id": prop_id, + "name": "Shield", + "prop_type_id": self.prop_type_id, + "description": "Updated", + } + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + + # Verify update + with self._app.get_db().sessionmaker() as session: + prop = session.get(Props, prop_id) + self.assertEqual("Shield", prop.name) + self.assertEqual("Updated", prop.description) + + def test_update_props_missing_id(self): + """Test PATCH returns 400 when id is missing.""" + response = self.fetch( + "/api/v1/show/stage/props", + method="PATCH", + body=tornado.escape.json_encode( + {"name": "Shield", "prop_type_id": self.prop_type_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("ID missing", response_body["message"]) + + def test_update_props_not_found(self): + """Test PATCH returns 404 for non-existent prop.""" + response = self.fetch( + "/api/v1/show/stage/props", + method="PATCH", + body=tornado.escape.json_encode( + {"id": 99999, "name": "Shield", "prop_type_id": self.prop_type_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(404, response.code) + + def test_update_props_missing_name(self): + """Test PATCH returns 400 when name is missing.""" + # Create a prop first + with self._app.get_db().sessionmaker() as session: + prop = Props( + show_id=self.show_id, + prop_type_id=self.prop_type_id, + name="Sword", + description="", + ) + session.add(prop) + session.flush() + prop_id = prop.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/props", + method="PATCH", + body=tornado.escape.json_encode( + {"id": prop_id, "prop_type_id": self.prop_type_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Name missing", response_body["message"]) + + def test_update_props_missing_prop_type_id(self): + """Test PATCH returns 400 when prop_type_id is missing.""" + # Create a prop first + with self._app.get_db().sessionmaker() as session: + prop = Props( + show_id=self.show_id, + prop_type_id=self.prop_type_id, + name="Sword", + description="", + ) + session.add(prop) + session.flush() + prop_id = prop.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/props", + method="PATCH", + body=tornado.escape.json_encode({"id": prop_id, "name": "Shield"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Prop type ID missing", response_body["message"]) + + def test_update_props_invalid_prop_type_id(self): + """Test PATCH returns 400 for non-integer prop_type_id.""" + # Create a prop first + with self._app.get_db().sessionmaker() as session: + prop = Props( + show_id=self.show_id, + prop_type_id=self.prop_type_id, + name="Sword", + description="", + ) + session.add(prop) + session.flush() + prop_id = prop.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/props", + method="PATCH", + body=tornado.escape.json_encode( + {"id": prop_id, "name": "Shield", "prop_type_id": "invalid"} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Invalid prop type ID", response_body["message"]) + + def test_update_props_prop_type_not_found(self): + """Test PATCH returns 404 for non-existent prop type.""" + # Create a prop first + with self._app.get_db().sessionmaker() as session: + prop = Props( + show_id=self.show_id, + prop_type_id=self.prop_type_id, + name="Sword", + description="", + ) + session.add(prop) + session.flush() + prop_id = prop.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/props", + method="PATCH", + body=tornado.escape.json_encode( + {"id": prop_id, "name": "Shield", "prop_type_id": 99999} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(404, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Prop type not found", response_body["message"]) + + def test_update_props_prop_type_wrong_show(self): + """Test PATCH returns 400 for prop type from different show.""" + # Create a prop first + with self._app.get_db().sessionmaker() as session: + prop = Props( + show_id=self.show_id, + prop_type_id=self.prop_type_id, + name="Sword", + description="", + ) + session.add(prop) + session.flush() + prop_id = prop.id + + # Create another show with a prop type + other_show = Show(name="Other Show", script_mode=ShowScriptType.FULL) + session.add(other_show) + session.flush() + + other_prop_type = PropType( + show_id=other_show.id, name="Other Type", description="" + ) + session.add(other_prop_type) + session.flush() + other_prop_type_id = other_prop_type.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/props", + method="PATCH", + body=tornado.escape.json_encode( + {"id": prop_id, "name": "Shield", "prop_type_id": other_prop_type_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Invalid prop type for show", response_body["message"]) + + def test_update_props_no_show(self): + """Test PATCH returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch( + "/api/v1/show/stage/props", + method="PATCH", + body=tornado.escape.json_encode( + {"id": 1, "name": "Shield", "prop_type_id": self.prop_type_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + + # DELETE tests + + def test_delete_props_success(self): + """Test DELETE removes a prop.""" + # Create a prop first + with self._app.get_db().sessionmaker() as session: + prop = Props( + show_id=self.show_id, + prop_type_id=self.prop_type_id, + name="Sword", + description="", + ) + session.add(prop) + session.flush() + prop_id = prop.id + session.commit() + + response = self.fetch( + f"/api/v1/show/stage/props?id={prop_id}", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + + # Verify deletion + with self._app.get_db().sessionmaker() as session: + prop = session.get(Props, prop_id) + self.assertIsNone(prop) + + def test_delete_props_missing_id(self): + """Test DELETE returns 400 when id is missing.""" + response = self.fetch( + "/api/v1/show/stage/props", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("ID missing", response_body["message"]) + + def test_delete_props_invalid_id(self): + """Test DELETE returns 400 for non-integer ID.""" + response = self.fetch( + "/api/v1/show/stage/props?id=invalid", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Invalid ID", response_body["message"]) + + def test_delete_props_not_found(self): + """Test DELETE returns 404 for non-existent prop.""" + response = self.fetch( + "/api/v1/show/stage/props?id=99999", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(404, response.code) + + def test_delete_props_no_show(self): + """Test DELETE returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch( + "/api/v1/show/stage/props?id=1", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) diff --git a/server/test/controllers/api/show/stage/test_scenery.py b/server/test/controllers/api/show/stage/test_scenery.py new file mode 100644 index 00000000..5298955c --- /dev/null +++ b/server/test/controllers/api/show/stage/test_scenery.py @@ -0,0 +1,1080 @@ +import tornado.escape + +from models.show import Act, Scene, Show, ShowScriptType +from models.stage import Scenery, SceneryAllocation, SceneryType +from models.user import User +from test.conftest import DigiScriptTestCase + + +class TestSceneryAllocationController(DigiScriptTestCase): + """Test suite for /api/v1/show/stage/scenery/allocations endpoint.""" + + def setUp(self): + super().setUp() + with self._app.get_db().sessionmaker() as session: + show = Show(name="Test Show", script_mode=ShowScriptType.FULL) + session.add(show) + session.flush() + self.show_id = show.id + + # Create an act and scene + act = Act(show_id=show.id, name="Act 1", interval_after=False) + session.add(act) + session.flush() + self.act_id = act.id + + scene = Scene( + show_id=show.id, + act_id=act.id, + name="Scene 1", + previous_scene_id=None, + ) + session.add(scene) + session.flush() + self.scene_id = scene.id + + # Create a scenery type and scenery + scenery_type = SceneryType( + show_id=show.id, name="Backdrops", description="" + ) + session.add(scenery_type) + session.flush() + self.scenery_type_id = scenery_type.id + + scenery = Scenery( + show_id=show.id, + scenery_type_id=scenery_type.id, + name="Forest Backdrop", + description="Test scenery", + ) + session.add(scenery) + session.flush() + self.scenery_id = scenery.id + + # Create admin user for RBAC + admin = User(username="admin", is_admin=True, password="test") + session.add(admin) + session.flush() + self.user_id = admin.id + + session.commit() + + self._app.digi_settings.settings["current_show"].set_value(self.show_id) + self.token = self._app.jwt_service.create_access_token( + data={"user_id": self.user_id} + ) + + def test_get_allocations_empty(self): + """Test GET with no allocations returns empty list.""" + response = self.fetch("/api/v1/show/stage/scenery/allocations") + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("allocations", response_body) + self.assertEqual([], response_body["allocations"]) + + def test_get_allocations_returns_all(self): + """Test GET returns all allocations for the show.""" + # Create an allocation + with self._app.get_db().sessionmaker() as session: + allocation = SceneryAllocation( + scenery_id=self.scenery_id, scene_id=self.scene_id + ) + session.add(allocation) + session.commit() + + response = self.fetch("/api/v1/show/stage/scenery/allocations") + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertEqual(1, len(response_body["allocations"])) + self.assertEqual(self.scenery_id, response_body["allocations"][0]["scenery_id"]) + self.assertEqual(self.scene_id, response_body["allocations"][0]["scene_id"]) + + def test_get_allocations_no_show(self): + """Test GET returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch("/api/v1/show/stage/scenery/allocations") + self.assertEqual(400, response.code) + + def test_create_allocation_success(self): + """Test POST creates a new allocation.""" + response = self.fetch( + "/api/v1/show/stage/scenery/allocations", + method="POST", + body=tornado.escape.json_encode( + {"scenery_id": self.scenery_id, "scene_id": self.scene_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("id", response_body) + self.assertIn("message", response_body) + + # Verify allocation was created + with self._app.get_db().sessionmaker() as session: + allocation = session.get(SceneryAllocation, response_body["id"]) + self.assertIsNotNone(allocation) + self.assertEqual(self.scenery_id, allocation.scenery_id) + self.assertEqual(self.scene_id, allocation.scene_id) + + def test_create_allocation_duplicate(self): + """Test POST returns 400 for duplicate allocation.""" + # Create initial allocation + with self._app.get_db().sessionmaker() as session: + allocation = SceneryAllocation( + scenery_id=self.scenery_id, scene_id=self.scene_id + ) + session.add(allocation) + session.commit() + + # Try to create duplicate + response = self.fetch( + "/api/v1/show/stage/scenery/allocations", + method="POST", + body=tornado.escape.json_encode( + {"scenery_id": self.scenery_id, "scene_id": self.scene_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("already exists", response_body["message"]) + + def test_create_allocation_invalid_scenery_id(self): + """Test POST returns 404 for non-existent scenery.""" + response = self.fetch( + "/api/v1/show/stage/scenery/allocations", + method="POST", + body=tornado.escape.json_encode( + {"scenery_id": 99999, "scene_id": self.scene_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(404, response.code) + + def test_create_allocation_invalid_scene_id(self): + """Test POST returns 404 for non-existent scene.""" + response = self.fetch( + "/api/v1/show/stage/scenery/allocations", + method="POST", + body=tornado.escape.json_encode( + {"scenery_id": self.scenery_id, "scene_id": 99999} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(404, response.code) + + def test_create_allocation_scenery_wrong_show(self): + """Test POST returns 404 for scenery from different show.""" + # Create another show with scenery + with self._app.get_db().sessionmaker() as session: + other_show = Show(name="Other Show", script_mode=ShowScriptType.FULL) + session.add(other_show) + session.flush() + + other_scenery_type = SceneryType( + show_id=other_show.id, name="Other Type", description="" + ) + session.add(other_scenery_type) + session.flush() + + other_scenery = Scenery( + show_id=other_show.id, + scenery_type_id=other_scenery_type.id, + name="Other Scenery", + description="", + ) + session.add(other_scenery) + session.flush() + other_scenery_id = other_scenery.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/scenery/allocations", + method="POST", + body=tornado.escape.json_encode( + {"scenery_id": other_scenery_id, "scene_id": self.scene_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(404, response.code) + + def test_create_allocation_missing_scenery_id(self): + """Test POST returns 400 when scenery_id is missing.""" + response = self.fetch( + "/api/v1/show/stage/scenery/allocations", + method="POST", + body=tornado.escape.json_encode({"scene_id": self.scene_id}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("scenery_id missing", response_body["message"]) + + def test_create_allocation_missing_scene_id(self): + """Test POST returns 400 when scene_id is missing.""" + response = self.fetch( + "/api/v1/show/stage/scenery/allocations", + method="POST", + body=tornado.escape.json_encode({"scenery_id": self.scenery_id}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("scene_id missing", response_body["message"]) + + def test_create_allocation_no_show(self): + """Test POST returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch( + "/api/v1/show/stage/scenery/allocations", + method="POST", + body=tornado.escape.json_encode( + {"scenery_id": self.scenery_id, "scene_id": self.scene_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + + def test_delete_allocation_success(self): + """Test DELETE removes an allocation.""" + # Create an allocation + with self._app.get_db().sessionmaker() as session: + allocation = SceneryAllocation( + scenery_id=self.scenery_id, scene_id=self.scene_id + ) + session.add(allocation) + session.flush() + allocation_id = allocation.id + session.commit() + + response = self.fetch( + f"/api/v1/show/stage/scenery/allocations?id={allocation_id}", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + + # Verify allocation was deleted + with self._app.get_db().sessionmaker() as session: + allocation = session.get(SceneryAllocation, allocation_id) + self.assertIsNone(allocation) + + def test_delete_allocation_not_found(self): + """Test DELETE returns 404 for non-existent allocation.""" + response = self.fetch( + "/api/v1/show/stage/scenery/allocations?id=99999", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(404, response.code) + + def test_delete_allocation_missing_id(self): + """Test DELETE returns 400 when ID is missing.""" + response = self.fetch( + "/api/v1/show/stage/scenery/allocations", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("ID missing", response_body["message"]) + + def test_delete_allocation_invalid_id(self): + """Test DELETE returns 400 for non-integer ID.""" + response = self.fetch( + "/api/v1/show/stage/scenery/allocations?id=invalid", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Invalid ID", response_body["message"]) + + def test_delete_allocation_no_show(self): + """Test DELETE returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch( + "/api/v1/show/stage/scenery/allocations?id=1", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + + +class TestSceneryTypesController(DigiScriptTestCase): + """Test suite for /api/v1/show/stage/scenery/types endpoint.""" + + def setUp(self): + super().setUp() + with self._app.get_db().sessionmaker() as session: + show = Show(name="Test Show", script_mode=ShowScriptType.FULL) + session.add(show) + session.flush() + self.show_id = show.id + + # Create admin user for RBAC + admin = User(username="admin", is_admin=True, password="test") + session.add(admin) + session.flush() + self.user_id = admin.id + + session.commit() + + self._app.digi_settings.settings["current_show"].set_value(self.show_id) + self.token = self._app.jwt_service.create_access_token( + data={"user_id": self.user_id} + ) + + # GET tests + + def test_get_scenery_types_empty(self): + """Test GET with no scenery types returns empty list.""" + response = self.fetch("/api/v1/show/stage/scenery/types") + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("scenery_types", response_body) + self.assertEqual([], response_body["scenery_types"]) + + def test_get_scenery_types_returns_all(self): + """Test GET returns all scenery types for the show.""" + with self._app.get_db().sessionmaker() as session: + scenery_type = SceneryType( + show_id=self.show_id, name="Backdrops", description="Background pieces" + ) + session.add(scenery_type) + session.commit() + + response = self.fetch("/api/v1/show/stage/scenery/types") + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertEqual(1, len(response_body["scenery_types"])) + self.assertEqual("Backdrops", response_body["scenery_types"][0]["name"]) + + def test_get_scenery_types_no_show(self): + """Test GET returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch("/api/v1/show/stage/scenery/types") + self.assertEqual(400, response.code) + + # POST tests + + def test_create_scenery_type_success(self): + """Test POST creates a new scenery type.""" + response = self.fetch( + "/api/v1/show/stage/scenery/types", + method="POST", + body=tornado.escape.json_encode({"name": "Platforms"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("id", response_body) + self.assertIn("message", response_body) + + # Verify scenery type was created + with self._app.get_db().sessionmaker() as session: + scenery_type = session.get(SceneryType, response_body["id"]) + self.assertIsNotNone(scenery_type) + self.assertEqual("Platforms", scenery_type.name) + + def test_create_scenery_type_with_description(self): + """Test POST creates a scenery type with description.""" + response = self.fetch( + "/api/v1/show/stage/scenery/types", + method="POST", + body=tornado.escape.json_encode( + {"name": "Platforms", "description": "Elevated surfaces"} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + + # Verify description was saved + with self._app.get_db().sessionmaker() as session: + scenery_type = session.get(SceneryType, response_body["id"]) + self.assertEqual("Elevated surfaces", scenery_type.description) + + def test_create_scenery_type_missing_name(self): + """Test POST returns 400 when name is missing.""" + response = self.fetch( + "/api/v1/show/stage/scenery/types", + method="POST", + body=tornado.escape.json_encode({"description": "Some description"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Name missing", response_body["message"]) + + def test_create_scenery_type_no_show(self): + """Test POST returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch( + "/api/v1/show/stage/scenery/types", + method="POST", + body=tornado.escape.json_encode({"name": "Platforms"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + + # PATCH tests + + def test_update_scenery_type_success(self): + """Test PATCH updates an existing scenery type.""" + # Create a scenery type first + with self._app.get_db().sessionmaker() as session: + scenery_type = SceneryType( + show_id=self.show_id, name="Backdrops", description="" + ) + session.add(scenery_type) + session.flush() + scenery_type_id = scenery_type.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/scenery/types", + method="PATCH", + body=tornado.escape.json_encode( + {"id": scenery_type_id, "name": "Flats", "description": "Updated"} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + + # Verify update + with self._app.get_db().sessionmaker() as session: + scenery_type = session.get(SceneryType, scenery_type_id) + self.assertEqual("Flats", scenery_type.name) + self.assertEqual("Updated", scenery_type.description) + + def test_update_scenery_type_missing_id(self): + """Test PATCH returns 400 when id is missing.""" + response = self.fetch( + "/api/v1/show/stage/scenery/types", + method="PATCH", + body=tornado.escape.json_encode({"name": "Flats"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("ID missing", response_body["message"]) + + def test_update_scenery_type_not_found(self): + """Test PATCH returns 404 for non-existent scenery type.""" + response = self.fetch( + "/api/v1/show/stage/scenery/types", + method="PATCH", + body=tornado.escape.json_encode({"id": 99999, "name": "Flats"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(404, response.code) + + def test_update_scenery_type_missing_name(self): + """Test PATCH returns 400 when name is missing.""" + # Create a scenery type first + with self._app.get_db().sessionmaker() as session: + scenery_type = SceneryType( + show_id=self.show_id, name="Backdrops", description="" + ) + session.add(scenery_type) + session.flush() + scenery_type_id = scenery_type.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/scenery/types", + method="PATCH", + body=tornado.escape.json_encode({"id": scenery_type_id}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Name missing", response_body["message"]) + + def test_update_scenery_type_no_show(self): + """Test PATCH returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch( + "/api/v1/show/stage/scenery/types", + method="PATCH", + body=tornado.escape.json_encode({"id": 1, "name": "Flats"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + + # DELETE tests + + def test_delete_scenery_type_success(self): + """Test DELETE removes a scenery type.""" + # Create a scenery type first + with self._app.get_db().sessionmaker() as session: + scenery_type = SceneryType( + show_id=self.show_id, name="Backdrops", description="" + ) + session.add(scenery_type) + session.flush() + scenery_type_id = scenery_type.id + session.commit() + + response = self.fetch( + f"/api/v1/show/stage/scenery/types?id={scenery_type_id}", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + + # Verify deletion + with self._app.get_db().sessionmaker() as session: + scenery_type = session.get(SceneryType, scenery_type_id) + self.assertIsNone(scenery_type) + + def test_delete_scenery_type_missing_id(self): + """Test DELETE returns 400 when id is missing.""" + response = self.fetch( + "/api/v1/show/stage/scenery/types", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("ID missing", response_body["message"]) + + def test_delete_scenery_type_invalid_id(self): + """Test DELETE returns 400 for non-integer ID.""" + response = self.fetch( + "/api/v1/show/stage/scenery/types?id=invalid", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Invalid ID", response_body["message"]) + + def test_delete_scenery_type_not_found(self): + """Test DELETE returns 404 for non-existent scenery type.""" + response = self.fetch( + "/api/v1/show/stage/scenery/types?id=99999", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(404, response.code) + + def test_delete_scenery_type_no_show(self): + """Test DELETE returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch( + "/api/v1/show/stage/scenery/types?id=1", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + + +class TestSceneryController(DigiScriptTestCase): + """Test suite for /api/v1/show/stage/scenery endpoint.""" + + def setUp(self): + super().setUp() + with self._app.get_db().sessionmaker() as session: + show = Show(name="Test Show", script_mode=ShowScriptType.FULL) + session.add(show) + session.flush() + self.show_id = show.id + + # Create a scenery type + scenery_type = SceneryType( + show_id=show.id, name="Backdrops", description="" + ) + session.add(scenery_type) + session.flush() + self.scenery_type_id = scenery_type.id + + # Create admin user for RBAC + admin = User(username="admin", is_admin=True, password="test") + session.add(admin) + session.flush() + self.user_id = admin.id + + session.commit() + + self._app.digi_settings.settings["current_show"].set_value(self.show_id) + self.token = self._app.jwt_service.create_access_token( + data={"user_id": self.user_id} + ) + + # GET tests + + def test_get_scenery_empty(self): + """Test GET with no scenery returns empty list.""" + response = self.fetch("/api/v1/show/stage/scenery") + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("scenery", response_body) + self.assertEqual([], response_body["scenery"]) + + def test_get_scenery_returns_all(self): + """Test GET returns all scenery for the show.""" + with self._app.get_db().sessionmaker() as session: + scenery = Scenery( + show_id=self.show_id, + scenery_type_id=self.scenery_type_id, + name="Forest Backdrop", + description="A woodland scene", + ) + session.add(scenery) + session.commit() + + response = self.fetch("/api/v1/show/stage/scenery") + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertEqual(1, len(response_body["scenery"])) + self.assertEqual("Forest Backdrop", response_body["scenery"][0]["name"]) + + def test_get_scenery_no_show(self): + """Test GET returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch("/api/v1/show/stage/scenery") + self.assertEqual(400, response.code) + + # POST tests + + def test_create_scenery_success(self): + """Test POST creates a new scenery.""" + response = self.fetch( + "/api/v1/show/stage/scenery", + method="POST", + body=tornado.escape.json_encode( + {"name": "Castle Wall", "scenery_type_id": self.scenery_type_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("id", response_body) + self.assertIn("message", response_body) + + # Verify scenery was created + with self._app.get_db().sessionmaker() as session: + scenery = session.get(Scenery, response_body["id"]) + self.assertIsNotNone(scenery) + self.assertEqual("Castle Wall", scenery.name) + self.assertEqual(self.scenery_type_id, scenery.scenery_type_id) + + def test_create_scenery_with_description(self): + """Test POST creates a scenery with description.""" + response = self.fetch( + "/api/v1/show/stage/scenery", + method="POST", + body=tornado.escape.json_encode( + { + "name": "Castle Wall", + "scenery_type_id": self.scenery_type_id, + "description": "Stone castle wall", + } + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + + # Verify description was saved + with self._app.get_db().sessionmaker() as session: + scenery = session.get(Scenery, response_body["id"]) + self.assertEqual("Stone castle wall", scenery.description) + + def test_create_scenery_missing_name(self): + """Test POST returns 400 when name is missing.""" + response = self.fetch( + "/api/v1/show/stage/scenery", + method="POST", + body=tornado.escape.json_encode({"scenery_type_id": self.scenery_type_id}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Name missing", response_body["message"]) + + def test_create_scenery_missing_scenery_type_id(self): + """Test POST returns 400 when scenery_type_id is missing.""" + response = self.fetch( + "/api/v1/show/stage/scenery", + method="POST", + body=tornado.escape.json_encode({"name": "Castle Wall"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Scenery type ID missing", response_body["message"]) + + def test_create_scenery_invalid_scenery_type_id(self): + """Test POST returns 400 for non-integer scenery_type_id.""" + response = self.fetch( + "/api/v1/show/stage/scenery", + method="POST", + body=tornado.escape.json_encode( + {"name": "Castle Wall", "scenery_type_id": "invalid"} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + # Note: The controller has a typo in the error message + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Scenery prop type ID", response_body["message"]) + + def test_create_scenery_scenery_type_not_found(self): + """Test POST returns 404 for non-existent scenery type.""" + response = self.fetch( + "/api/v1/show/stage/scenery", + method="POST", + body=tornado.escape.json_encode( + {"name": "Castle Wall", "scenery_type_id": 99999} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(404, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Scenery type not found", response_body["message"]) + + def test_create_scenery_scenery_type_wrong_show(self): + """Test POST returns 400 for scenery type from different show.""" + # Create another show with a scenery type + with self._app.get_db().sessionmaker() as session: + other_show = Show(name="Other Show", script_mode=ShowScriptType.FULL) + session.add(other_show) + session.flush() + + other_scenery_type = SceneryType( + show_id=other_show.id, name="Other Type", description="" + ) + session.add(other_scenery_type) + session.flush() + other_scenery_type_id = other_scenery_type.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/scenery", + method="POST", + body=tornado.escape.json_encode( + {"name": "Castle Wall", "scenery_type_id": other_scenery_type_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Invalid scenery type for show", response_body["message"]) + + def test_create_scenery_no_show(self): + """Test POST returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch( + "/api/v1/show/stage/scenery", + method="POST", + body=tornado.escape.json_encode( + {"name": "Castle Wall", "scenery_type_id": self.scenery_type_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + + # PATCH tests + + def test_update_scenery_success(self): + """Test PATCH updates an existing scenery.""" + # Create a scenery first + with self._app.get_db().sessionmaker() as session: + scenery = Scenery( + show_id=self.show_id, + scenery_type_id=self.scenery_type_id, + name="Castle Wall", + description="", + ) + session.add(scenery) + session.flush() + scenery_id = scenery.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/scenery", + method="PATCH", + body=tornado.escape.json_encode( + { + "id": scenery_id, + "name": "Stone Wall", + "scenery_type_id": self.scenery_type_id, + "description": "Updated", + } + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + + # Verify update + with self._app.get_db().sessionmaker() as session: + scenery = session.get(Scenery, scenery_id) + self.assertEqual("Stone Wall", scenery.name) + self.assertEqual("Updated", scenery.description) + + def test_update_scenery_missing_id(self): + """Test PATCH returns 400 when id is missing.""" + response = self.fetch( + "/api/v1/show/stage/scenery", + method="PATCH", + body=tornado.escape.json_encode( + {"name": "Stone Wall", "scenery_type_id": self.scenery_type_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("ID missing", response_body["message"]) + + def test_update_scenery_not_found(self): + """Test PATCH returns 404 for non-existent scenery.""" + response = self.fetch( + "/api/v1/show/stage/scenery", + method="PATCH", + body=tornado.escape.json_encode( + { + "id": 99999, + "name": "Stone Wall", + "scenery_type_id": self.scenery_type_id, + } + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(404, response.code) + + def test_update_scenery_missing_name(self): + """Test PATCH returns 400 when name is missing.""" + # Create a scenery first + with self._app.get_db().sessionmaker() as session: + scenery = Scenery( + show_id=self.show_id, + scenery_type_id=self.scenery_type_id, + name="Castle Wall", + description="", + ) + session.add(scenery) + session.flush() + scenery_id = scenery.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/scenery", + method="PATCH", + body=tornado.escape.json_encode( + {"id": scenery_id, "scenery_type_id": self.scenery_type_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Name missing", response_body["message"]) + + def test_update_scenery_missing_scenery_type_id(self): + """Test PATCH returns 400 when scenery_type_id is missing.""" + # Create a scenery first + with self._app.get_db().sessionmaker() as session: + scenery = Scenery( + show_id=self.show_id, + scenery_type_id=self.scenery_type_id, + name="Castle Wall", + description="", + ) + session.add(scenery) + session.flush() + scenery_id = scenery.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/scenery", + method="PATCH", + body=tornado.escape.json_encode({"id": scenery_id, "name": "Stone Wall"}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Scenery type ID missing", response_body["message"]) + + def test_update_scenery_invalid_scenery_type_id(self): + """Test PATCH returns 400 for non-integer scenery_type_id.""" + # Create a scenery first + with self._app.get_db().sessionmaker() as session: + scenery = Scenery( + show_id=self.show_id, + scenery_type_id=self.scenery_type_id, + name="Castle Wall", + description="", + ) + session.add(scenery) + session.flush() + scenery_id = scenery.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/scenery", + method="PATCH", + body=tornado.escape.json_encode( + {"id": scenery_id, "name": "Stone Wall", "scenery_type_id": "invalid"} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + # Note: The controller has a typo in the error message + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Scenery prop type ID", response_body["message"]) + + def test_update_scenery_scenery_type_not_found(self): + """Test PATCH returns 404 for non-existent scenery type.""" + # Create a scenery first + with self._app.get_db().sessionmaker() as session: + scenery = Scenery( + show_id=self.show_id, + scenery_type_id=self.scenery_type_id, + name="Castle Wall", + description="", + ) + session.add(scenery) + session.flush() + scenery_id = scenery.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/scenery", + method="PATCH", + body=tornado.escape.json_encode( + {"id": scenery_id, "name": "Stone Wall", "scenery_type_id": 99999} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(404, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Scenery type not found", response_body["message"]) + + def test_update_scenery_scenery_type_wrong_show(self): + """Test PATCH returns 400 for scenery type from different show.""" + # Create a scenery first + with self._app.get_db().sessionmaker() as session: + scenery = Scenery( + show_id=self.show_id, + scenery_type_id=self.scenery_type_id, + name="Castle Wall", + description="", + ) + session.add(scenery) + session.flush() + scenery_id = scenery.id + + # Create another show with a scenery type + other_show = Show(name="Other Show", script_mode=ShowScriptType.FULL) + session.add(other_show) + session.flush() + + other_scenery_type = SceneryType( + show_id=other_show.id, name="Other Type", description="" + ) + session.add(other_scenery_type) + session.flush() + other_scenery_type_id = other_scenery_type.id + session.commit() + + response = self.fetch( + "/api/v1/show/stage/scenery", + method="PATCH", + body=tornado.escape.json_encode( + { + "id": scenery_id, + "name": "Stone Wall", + "scenery_type_id": other_scenery_type_id, + } + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Invalid scenery type for show", response_body["message"]) + + def test_update_scenery_no_show(self): + """Test PATCH returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch( + "/api/v1/show/stage/scenery", + method="PATCH", + body=tornado.escape.json_encode( + {"id": 1, "name": "Stone Wall", "scenery_type_id": self.scenery_type_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + + # DELETE tests + + def test_delete_scenery_success(self): + """Test DELETE removes a scenery.""" + # Create a scenery first + with self._app.get_db().sessionmaker() as session: + scenery = Scenery( + show_id=self.show_id, + scenery_type_id=self.scenery_type_id, + name="Castle Wall", + description="", + ) + session.add(scenery) + session.flush() + scenery_id = scenery.id + session.commit() + + response = self.fetch( + f"/api/v1/show/stage/scenery?id={scenery_id}", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(200, response.code) + + # Verify deletion + with self._app.get_db().sessionmaker() as session: + scenery = session.get(Scenery, scenery_id) + self.assertIsNone(scenery) + + def test_delete_scenery_missing_id(self): + """Test DELETE returns 400 when id is missing.""" + response = self.fetch( + "/api/v1/show/stage/scenery", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("ID missing", response_body["message"]) + + def test_delete_scenery_invalid_id(self): + """Test DELETE returns 400 for non-integer ID.""" + response = self.fetch( + "/api/v1/show/stage/scenery?id=invalid", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Invalid ID", response_body["message"]) + + def test_delete_scenery_not_found(self): + """Test DELETE returns 404 for non-existent scenery.""" + response = self.fetch( + "/api/v1/show/stage/scenery?id=99999", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(404, response.code) + + def test_delete_scenery_no_show(self): + """Test DELETE returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch( + "/api/v1/show/stage/scenery?id=1", + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + self.assertEqual(400, response.code) From ea903fa1091315d8a2944d4cf1b4a9404f294c4a Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Thu, 29 Jan 2026 00:18:10 +0000 Subject: [PATCH 06/43] Update electron package dependencies --- electron/package-lock.json | 8 ++++---- electron/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/electron/package-lock.json b/electron/package-lock.json index c8e53e08..4328e0e0 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -23,7 +23,7 @@ "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.5", - "globals": "^17.1.0", + "globals": "^17.2.0", "prettier": "^3.8.1" }, "engines": { @@ -4452,9 +4452,9 @@ } }, "node_modules/globals": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.1.0.tgz", - "integrity": "sha512-8HoIcWI5fCvG5NADj4bDav+er9B9JMj2vyL2pI8D0eismKyUvPLTSs+Ln3wqhwcp306i73iyVnEKx3F6T47TGw==", + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.2.0.tgz", + "integrity": "sha512-tovnCz/fEq+Ripoq+p/gN1u7l6A7wwkoBT9pRCzTHzsD/LvADIzXZdjmRymh5Ztf0DYC3Rwg5cZRYjxzBmzbWg==", "dev": true, "license": "MIT", "engines": { diff --git a/electron/package.json b/electron/package.json index 8c3a8319..6418bbe9 100644 --- a/electron/package.json +++ b/electron/package.json @@ -39,7 +39,7 @@ "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.5", - "globals": "^17.1.0", + "globals": "^17.2.0", "prettier": "^3.8.1" }, "config": { From 108871bd99d3d71d6dda229d6cd563bef346b91f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:06:15 +0000 Subject: [PATCH 07/43] Bump alembic from 1.18.1 to 1.18.2 in /server (#887) Bumps [alembic](https://github.com/sqlalchemy/alembic) from 1.18.1 to 1.18.2. - [Release notes](https://github.com/sqlalchemy/alembic/releases) - [Changelog](https://github.com/sqlalchemy/alembic/blob/main/CHANGES) - [Commits](https://github.com/sqlalchemy/alembic/commits) --- updated-dependencies: - dependency-name: alembic dependency-version: 1.18.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- server/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/requirements.txt b/server/requirements.txt index 1d8a2d0b..a2fde405 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -6,7 +6,7 @@ marshmallow-sqlalchemy>=1.4.0 tornado-prometheus==0.1.2 bcrypt==4.3.0 anytree==2.13.0 -alembic==1.18.1 +alembic==1.18.2 marshmallow<5 pyjwt[crypto]==2.10.1 setuptools==80.10.2 From 3985876425f87a78d6e05722b5cda8662acc1b39 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Fri, 30 Jan 2026 00:39:42 +0000 Subject: [PATCH 08/43] Update electron package dependencies --- electron/package-lock.json | 8 ++++---- electron/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/electron/package-lock.json b/electron/package-lock.json index 4328e0e0..d43794b2 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -19,7 +19,7 @@ "@electron-forge/maker-squirrel": "^7.11.1", "@electron-forge/maker-zip": "^7.11.1", "@eslint/js": "^9.39.2", - "electron": "^40.0.0", + "electron": "^40.1.0", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.5", @@ -2869,9 +2869,9 @@ "license": "MIT" }, "node_modules/electron": { - "version": "40.0.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-40.0.0.tgz", - "integrity": "sha512-UyBy5yJ0/wm4gNugCtNPjvddjAknMTuXR2aCHioXicH7aKRKGDBPp4xqTEi/doVcB3R+MN3wfU9o8d/9pwgK2A==", + "version": "40.1.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-40.1.0.tgz", + "integrity": "sha512-2j/kvw7uF0H1PnzYBzw2k2Q6q16J8ToKrtQzZfsAoXbbMY0l5gQi2DLOauIZLzwp4O01n8Wt/74JhSRwG0yj9A==", "dev": true, "hasInstallScript": true, "license": "MIT", diff --git a/electron/package.json b/electron/package.json index 6418bbe9..c0c48919 100644 --- a/electron/package.json +++ b/electron/package.json @@ -29,7 +29,7 @@ "bonjour-service": "^1.3.0" }, "devDependencies": { - "electron": "^40.0.0", + "electron": "^40.1.0", "@electron-forge/cli": "^7.11.1", "@electron-forge/maker-squirrel": "^7.11.1", "@electron-forge/maker-zip": "^7.11.1", From cb618409cfdc3d610bdf12247a8f68a9c7080ccb Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Fri, 30 Jan 2026 01:44:49 +0000 Subject: [PATCH 09/43] Refactor frontend components to use Promise.all for parallel data fetching (#890) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converts sequential await chains to parallel Promise.all calls in 10 Vue components, significantly improving page load times by running independent API calls concurrently. High priority refactors (largest impact): - ScriptEditor.vue: 14 sequential awaits → parallel groups - CueEditor.vue: 13 sequential awaits → parallel groups - ConfigMics.vue: 6 sequential awaits → single Promise.all Medium priority refactors: - ConfigSystem.vue: 4 parallel fetches - ConfigCharacters.vue: 2 parallel fetches - CharacterGroups.vue: 2 parallel fetches - PropsList.vue: 2 parallel fetches - SceneryList.vue: 2 parallel fetches Additional optimizations: - App.vue: Parallelize RBAC roles with WebSocket state check; parallelize user RBAC and settings after user fetch - ShowLiveView.vue: Parallelize session data and act list fetches Co-authored-by: Claude Opus 4.5 --- client/src/App.vue | 14 +++--- client/src/views/show/ShowLiveView.vue | 5 +-- .../views/show/config/ConfigCharacters.vue | 3 +- client/src/views/show/config/ConfigMics.vue | 14 +++--- .../vue_components/config/ConfigSystem.vue | 10 +++-- .../config/characters/CharacterGroups.vue | 3 +- .../show/config/cues/CueEditor.vue | 44 +++++++++--------- .../show/config/script/ScriptEditor.vue | 45 ++++++++++--------- .../show/config/stage/PropsList.vue | 3 +- .../show/config/stage/SceneryList.vue | 3 +- 10 files changed, 68 insertions(+), 76 deletions(-) diff --git a/client/src/App.vue b/client/src/App.vue index 27fe71f4..8c243c9b 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -324,19 +324,15 @@ export default { async awaitWSConnect() { if (this.WEBSOCKET_HEALTHY) { clearTimeout(this.loadTimer); - await this.GET_RBAC_ROLES(); - // Check WebSocket state for any pending operations - if (this.WEBSOCKET_HAS_PENDING_OPERATIONS) { - await this.CHECK_WEBSOCKET_STATE(); - } + await Promise.all([ + this.GET_RBAC_ROLES(), + this.WEBSOCKET_HAS_PENDING_OPERATIONS ? this.CHECK_WEBSOCKET_STATE() : Promise.resolve(), + ]); - // Check for authentication via token first if (this.AUTH_TOKEN) { - // Then get user data await this.GET_CURRENT_USER(); - await this.GET_CURRENT_RBAC(); - await this.GET_USER_SETTINGS(); + await Promise.all([this.GET_CURRENT_RBAC(), this.GET_USER_SETTINGS()]); } if (this.SETTINGS.current_show != null) { diff --git a/client/src/views/show/ShowLiveView.vue b/client/src/views/show/ShowLiveView.vue index 18940bd9..b657fde7 100644 --- a/client/src/views/show/ShowLiveView.vue +++ b/client/src/views/show/ShowLiveView.vue @@ -166,16 +166,13 @@ export default { }, }, async mounted() { - await this.GET_SHOW_SESSION_DATA(); + await Promise.all([this.GET_SHOW_SESSION_DATA(), this.GET_ACT_LIST()]); this.loadedSessionData = true; if (this.CURRENT_SHOW_INTERVAL != null) { this.setupIntervalTimer(); } - // Load act list (needed for interval overlay heading) - await this.GET_ACT_LIST(); - // Setup elapsed time tracking this.updateElapsedTime(); this.startTime = this.createDateAsUTC( diff --git a/client/src/views/show/config/ConfigCharacters.vue b/client/src/views/show/config/ConfigCharacters.vue index bbb55407..74dd670c 100644 --- a/client/src/views/show/config/ConfigCharacters.vue +++ b/client/src/views/show/config/ConfigCharacters.vue @@ -218,8 +218,7 @@ export default { }, }, async mounted() { - await this.GET_CHARACTER_LIST(); - await this.GET_CAST_LIST(); + await Promise.all([this.GET_CHARACTER_LIST(), this.GET_CAST_LIST()]); }, methods: { resetNewForm() { diff --git a/client/src/views/show/config/ConfigMics.vue b/client/src/views/show/config/ConfigMics.vue index 671bc213..413ce742 100644 --- a/client/src/views/show/config/ConfigMics.vue +++ b/client/src/views/show/config/ConfigMics.vue @@ -52,12 +52,14 @@ export default { }; }, async mounted() { - await this.GET_SCENE_LIST(); - await this.GET_ACT_LIST(); - await this.GET_CHARACTER_LIST(); - await this.GET_CAST_LIST(); - await this.GET_MICROPHONE_LIST(); - await this.GET_MIC_ALLOCATIONS(); + await Promise.all([ + this.GET_SCENE_LIST(), + this.GET_ACT_LIST(), + this.GET_CHARACTER_LIST(), + this.GET_CAST_LIST(), + this.GET_MICROPHONE_LIST(), + this.GET_MIC_ALLOCATIONS(), + ]); this.loaded = true; }, methods: { diff --git a/client/src/vue_components/config/ConfigSystem.vue b/client/src/vue_components/config/ConfigSystem.vue index 8c3342e4..bbb3743b 100644 --- a/client/src/vue_components/config/ConfigSystem.vue +++ b/client/src/vue_components/config/ConfigSystem.vue @@ -301,10 +301,12 @@ export default { ...mapGetters(['SCRIPT_MODES']), }, async mounted() { - await this.getAvailableShows(); - await this.getConnectedClients(); - await this.GET_SCRIPT_MODES(); - await this.getVersionStatus(); + await Promise.all([ + this.getAvailableShows(), + this.getConnectedClients(), + this.GET_SCRIPT_MODES(), + this.getVersionStatus(), + ]); this.loading = false; // Start time update interval for reactive "time ago" display diff --git a/client/src/vue_components/show/config/characters/CharacterGroups.vue b/client/src/vue_components/show/config/characters/CharacterGroups.vue index e77ec8b4..3fea250d 100644 --- a/client/src/vue_components/show/config/characters/CharacterGroups.vue +++ b/client/src/vue_components/show/config/characters/CharacterGroups.vue @@ -207,8 +207,7 @@ export default { ...mapGetters(['CHARACTER_LIST', 'CHARACTER_GROUP_LIST', 'IS_SHOW_EDITOR']), }, async mounted() { - await this.GET_CHARACTER_LIST(); - await this.GET_CHARACTER_GROUP_LIST(); + await Promise.all([this.GET_CHARACTER_LIST(), this.GET_CHARACTER_GROUP_LIST()]); this.loading = false; }, methods: { diff --git a/client/src/vue_components/show/config/cues/CueEditor.vue b/client/src/vue_components/show/config/cues/CueEditor.vue index b1e3c8a4..c424147c 100644 --- a/client/src/vue_components/show/config/cues/CueEditor.vue +++ b/client/src/vue_components/show/config/cues/CueEditor.vue @@ -183,30 +183,28 @@ export default { }, }, async beforeMount() { - // Get the current user - await this.GET_CURRENT_USER(); - // Config status - await this.GET_SCRIPT_CONFIG_STATUS(); - // Show details - await this.GET_ACT_LIST(); - await this.GET_SCENE_LIST(); - await this.GET_CHARACTER_LIST(); - await this.GET_CHARACTER_GROUP_LIST(); - await this.GET_CUE_TYPES(); - await this.LOAD_CUES(); - await this.GET_CUTS(); - await this.GET_STAGE_DIRECTION_STYLES(); + await Promise.all([ + this.GET_CURRENT_USER().then(() => { + if (this.CURRENT_USER != null) { + return Promise.all([ + this.GET_STAGE_DIRECTION_STYLE_OVERRIDES(), + this.GET_CUE_COLOUR_OVERRIDES(), + ]); + } + return Promise.resolve(); + }), + this.GET_SCRIPT_CONFIG_STATUS(), + this.GET_ACT_LIST(), + this.GET_SCENE_LIST(), + this.GET_CHARACTER_LIST(), + this.GET_CHARACTER_GROUP_LIST(), + this.GET_CUE_TYPES(), + this.LOAD_CUES(), + this.GET_CUTS(), + this.GET_STAGE_DIRECTION_STYLES(), + this.getMaxScriptPage(), + ]); - // User related stuff - if (this.CURRENT_USER != null) { - await this.GET_STAGE_DIRECTION_STYLE_OVERRIDES(); - await this.GET_CUE_COLOUR_OVERRIDES(); - } - - // Get the max page of the saved version of the script - await this.getMaxScriptPage(); - - // Initialisation of page data // Initialisation of page data const storedPage = localStorage.getItem('cueEditPage'); if (storedPage != null) { diff --git a/client/src/vue_components/show/config/script/ScriptEditor.vue b/client/src/vue_components/show/config/script/ScriptEditor.vue index eb2ab219..9eb7386f 100644 --- a/client/src/vue_components/show/config/script/ScriptEditor.vue +++ b/client/src/vue_components/show/config/script/ScriptEditor.vue @@ -325,29 +325,30 @@ export default { }, }, async beforeMount() { - // Get the current user - await this.GET_CURRENT_USER(); - await this.GET_USER_SETTINGS(); - // Config status - await this.GET_SCRIPT_CONFIG_STATUS(); - // Show details - await this.GET_ACT_LIST(); - await this.GET_SCENE_LIST(); - await this.GET_CHARACTER_LIST(); - await this.GET_CHARACTER_GROUP_LIST(); - // Stage direction styles - await this.GET_STAGE_DIRECTION_STYLES(); - // User related stuff - if (this.CURRENT_USER != null) { - await this.GET_STAGE_DIRECTION_STYLE_OVERRIDES(); - await this.GET_CUE_COLOUR_OVERRIDES(); - } - // Handle script cuts - await this.GET_CUTS(); - this.resetCutsToSaved(); + await Promise.all([ + this.GET_CURRENT_USER() + .then(() => this.GET_USER_SETTINGS()) + .then(() => { + if (this.CURRENT_USER != null) { + return Promise.all([ + this.GET_STAGE_DIRECTION_STYLE_OVERRIDES(), + this.GET_CUE_COLOUR_OVERRIDES(), + ]); + } + return Promise.resolve(); + }), + this.GET_SCRIPT_CONFIG_STATUS(), + this.GET_ACT_LIST(), + this.GET_SCENE_LIST(), + this.GET_CHARACTER_LIST(), + this.GET_CHARACTER_GROUP_LIST(), + this.GET_STAGE_DIRECTION_STYLES(), + this.GET_CUTS(), + this.getMaxScriptPage(), + ]); - // Get the max page of the saved version of the script - await this.getMaxScriptPage(); + // Handle script cuts (depends on GET_CUTS completing) + this.resetCutsToSaved(); // Initialisation of page data const storedPage = localStorage.getItem('scriptEditPage'); diff --git a/client/src/vue_components/show/config/stage/PropsList.vue b/client/src/vue_components/show/config/stage/PropsList.vue index ef0b82bf..37fa495b 100644 --- a/client/src/vue_components/show/config/stage/PropsList.vue +++ b/client/src/vue_components/show/config/stage/PropsList.vue @@ -337,8 +337,7 @@ export default { ...mapGetters(['PROPS_LIST', 'PROP_TYPES', 'IS_SHOW_EDITOR', 'PROP_TYPE_BY_ID']), }, async mounted() { - await this.GET_PROP_TYPES(); - await this.GET_PROPS_LIST(); + await Promise.all([this.GET_PROP_TYPES(), this.GET_PROPS_LIST()]); }, methods: { resetNewPropTypeForm() { diff --git a/client/src/vue_components/show/config/stage/SceneryList.vue b/client/src/vue_components/show/config/stage/SceneryList.vue index 1ca44dab..b61363df 100644 --- a/client/src/vue_components/show/config/stage/SceneryList.vue +++ b/client/src/vue_components/show/config/stage/SceneryList.vue @@ -348,8 +348,7 @@ export default { ...mapGetters(['SCENERY_LIST', 'SCENERY_TYPES', 'IS_SHOW_EDITOR', 'SCENERY_TYPE_BY_ID']), }, async mounted() { - await this.GET_SCENERY_TYPES(); - await this.GET_SCENERY_LIST(); + await Promise.all([this.GET_SCENERY_TYPES(), this.GET_SCENERY_LIST()]); }, methods: { resetNewSceneryTypeForm() { From 66bbf372de827c043f4de311dbb19c293b01935a Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Fri, 30 Jan 2026 03:00:54 +0000 Subject: [PATCH 10/43] Add hover colour change to active/next scene cards --- client/src/vue_components/show/live/StageManagerPane.vue | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/src/vue_components/show/live/StageManagerPane.vue b/client/src/vue_components/show/live/StageManagerPane.vue index 75341f58..107975b5 100644 --- a/client/src/vue_components/show/live/StageManagerPane.vue +++ b/client/src/vue_components/show/live/StageManagerPane.vue @@ -376,6 +376,14 @@ export default { background-color: #1a3147; } +.current-scene .scene-header:hover { + background-color: #215d35; +} + +.next-scene .scene-header:hover { + background-color: #28476b; +} + .scene-title { font-size: 0.8rem; font-weight: 500; From 732305336bfb60635bb7d6821ab94cb01818d7e3 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Fri, 30 Jan 2026 14:25:56 +0000 Subject: [PATCH 11/43] Fix duplicate HTML IDs to pass SonarQube quality gate (#892) * Fix duplicate HTML IDs across Vue modal components Resolves SonarQube reliability issues by ensuring unique HTML IDs in components with both 'add' and 'edit' modals. Each form element now uses context-specific prefixes (new-/edit-) to prevent accessibility issues and JavaScript selector conflicts. Also adds explanatory comment to intentionally empty catch block in ServerSelector.vue URL validator. Co-Authored-By: Claude Opus 4.5 * Fix duplicate HTML IDs in SceneryList.vue Add unique prefixes to form element IDs in the new-scenery-type, edit-scenery-type, new-scenery, and edit-scenery modals. Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- client/src/views/electron/ServerSelector.vue | 1 + client/src/views/show/config/ConfigCast.vue | 56 +++++--- .../views/show/config/ConfigCharacters.vue | 50 ++++--- client/src/views/show/config/ConfigCues.vue | 64 ++++----- .../config/acts_and_scenes/ConfigActs.vue | 58 ++++---- .../config/acts_and_scenes/ConfigScenes.vue | 54 ++++---- .../config/characters/CharacterGroups.vue | 56 ++++---- .../show/config/cues/ScriptLineCueEditor.vue | 36 ++--- .../show/config/mics/MicList.vue | 44 +++---- .../config/script/StageDirectionStyles.vue | 100 +++++++------- .../show/config/stage/CrewList.vue | 56 +++++--- .../show/config/stage/PropsList.vue | 120 +++++++++-------- .../show/config/stage/SceneryList.vue | 124 ++++++++++-------- .../user/settings/CueColourPreferences.vue | 24 ++-- .../user/settings/StageDirectionStyles.vue | 76 ++++++----- 15 files changed, 513 insertions(+), 406 deletions(-) diff --git a/client/src/views/electron/ServerSelector.vue b/client/src/views/electron/ServerSelector.vue index cc3cb033..9ba8eb00 100644 --- a/client/src/views/electron/ServerSelector.vue +++ b/client/src/views/electron/ServerSelector.vue @@ -350,6 +350,7 @@ const validUrl = (value) => { const url = new URL(value.startsWith('http') ? value : `http://${value}`); return url.protocol === 'http:' || url.protocol === 'https:'; } catch { + // Invalid URL syntax - return false for validation return false; } }; diff --git a/client/src/views/show/config/ConfigCast.vue b/client/src/views/show/config/ConfigCast.vue index 5a274225..39b0e7f7 100644 --- a/client/src/views/show/config/ConfigCast.vue +++ b/client/src/views/show/config/ConfigCast.vue @@ -62,27 +62,35 @@ @ok="onSubmitNew" > - + - + This is a required field. - + - + This is a required field. @@ -98,27 +106,35 @@ @ok="onSubmitEdit" > - + - + This is a required field. - + - + This is a required field. diff --git a/client/src/views/show/config/ConfigCharacters.vue b/client/src/views/show/config/ConfigCharacters.vue index 74dd670c..ffe45695 100644 --- a/client/src/views/show/config/ConfigCharacters.vue +++ b/client/src/views/show/config/ConfigCharacters.vue @@ -73,32 +73,37 @@ @ok="onSubmitNew" > - + - + This is a required field. - + - + - + This is a required field. - + - + - + This is a required field and must be 5 characters or less. - + This is a required field and must be 100 characters or less. - + - + This is a required field. @@ -123,44 +123,44 @@ @ok="onSubmitEditCueType" > - + - + This is a required field and must be 5 characters or less. - + This is a required field and must be 100 characters or less. - + - + This is a required field. diff --git a/client/src/vue_components/show/config/acts_and_scenes/ConfigActs.vue b/client/src/vue_components/show/config/acts_and_scenes/ConfigActs.vue index 37af19e6..e1487819 100644 --- a/client/src/vue_components/show/config/acts_and_scenes/ConfigActs.vue +++ b/client/src/vue_components/show/config/acts_and_scenes/ConfigActs.vue @@ -67,35 +67,39 @@ @ok="onSubmitNew" > - + - + This is a required field. - + @@ -110,38 +114,42 @@ @ok="onSubmitEdit" > - + - + This is a required field. - + - + This cannot form a circular dependency between acts. diff --git a/client/src/vue_components/show/config/acts_and_scenes/ConfigScenes.vue b/client/src/vue_components/show/config/acts_and_scenes/ConfigScenes.vue index 568e12f9..78d1cc18 100644 --- a/client/src/vue_components/show/config/acts_and_scenes/ConfigScenes.vue +++ b/client/src/vue_components/show/config/acts_and_scenes/ConfigScenes.vue @@ -91,40 +91,40 @@ @ok="onSubmitNew" > - + - + This is a required field. - + - + This is a required field. @@ -139,44 +139,44 @@ @ok="onSubmitEdit" > - + - + This is a required field. - + - + This is a required field. - + This cannot form a circular dependency between scenes. diff --git a/client/src/vue_components/show/config/characters/CharacterGroups.vue b/client/src/vue_components/show/config/characters/CharacterGroups.vue index 3fea250d..1788981a 100644 --- a/client/src/vue_components/show/config/characters/CharacterGroups.vue +++ b/client/src/vue_components/show/config/characters/CharacterGroups.vue @@ -62,35 +62,39 @@ @ok="onSubmitNew" > - + - + This is a required field. - + - + - + This is a required field. - + - + - + This is a required field. - + - + This is a required field. @@ -208,27 +208,27 @@ @ok="onSubmitEdit" > - + - + This is a required field. - + - + This is a required field. diff --git a/client/src/vue_components/show/config/mics/MicList.vue b/client/src/vue_components/show/config/mics/MicList.vue index e5625f94..8621d678 100644 --- a/client/src/vue_components/show/config/mics/MicList.vue +++ b/client/src/vue_components/show/config/mics/MicList.vue @@ -48,31 +48,31 @@ @ok="onSubmitNewMicrophone" > - + - + This is a required field, and must be unique. - + Something went wrong! @@ -88,31 +88,31 @@ @ok="onSubmitEditMicrophone" > - + - + This is a required field, and must be unique. - + Something went wrong! diff --git a/client/src/vue_components/show/config/script/StageDirectionStyles.vue b/client/src/vue_components/show/config/script/StageDirectionStyles.vue index 6d7cabeb..220b3e65 100644 --- a/client/src/vue_components/show/config/script/StageDirectionStyles.vue +++ b/client/src/vue_components/show/config/script/StageDirectionStyles.vue @@ -39,23 +39,27 @@

Configuration Options

- + This is a required field. - - + + Default @@ -81,41 +85,41 @@ - + This is a required field. @@ -150,23 +154,27 @@

Configuration Options

- + This is a required field. - - + + Default @@ -192,41 +200,41 @@ - + This is a required field. diff --git a/client/src/vue_components/show/config/stage/CrewList.vue b/client/src/vue_components/show/config/stage/CrewList.vue index d1c0b566..0a5388c6 100644 --- a/client/src/vue_components/show/config/stage/CrewList.vue +++ b/client/src/vue_components/show/config/stage/CrewList.vue @@ -38,27 +38,35 @@ @ok="onSubmitNew" > - + - + This is a required field. - + - + This is a required field. @@ -73,27 +81,35 @@ @ok="onSubmitEdit" > - + - + This is a required field. - + - + This is a required field. diff --git a/client/src/vue_components/show/config/stage/PropsList.vue b/client/src/vue_components/show/config/stage/PropsList.vue index 37fa495b..533d2abf 100644 --- a/client/src/vue_components/show/config/stage/PropsList.vue +++ b/client/src/vue_components/show/config/stage/PropsList.vue @@ -77,31 +77,35 @@ @ok="onSubmitNewPropType" > - + - + This is a required field. - + This is a required field. @@ -116,31 +120,35 @@ @ok="onSubmitEditPropType" > - + - + This is a required field. - + This is a required field. @@ -156,43 +164,47 @@ @ok="onSubmitNewProp" > - + - + This is a required field. - + - + This is a required field. - + This is a required field. @@ -207,43 +219,47 @@ @ok="onSubmitEditProp" > - + - + This is a required field. - + - + This is a required field. - + This is a required field. diff --git a/client/src/vue_components/show/config/stage/SceneryList.vue b/client/src/vue_components/show/config/stage/SceneryList.vue index b61363df..4d686482 100644 --- a/client/src/vue_components/show/config/stage/SceneryList.vue +++ b/client/src/vue_components/show/config/stage/SceneryList.vue @@ -77,31 +77,35 @@ @ok="onSubmitNewSceneryType" > - + - + This is a required field. - + This is a required field. @@ -116,31 +120,35 @@ @ok="onSubmitEditSceneryType" > - + - + This is a required field. - + This is a required field. @@ -157,46 +165,50 @@ > - + This is a required field. - + - + This is a required field. - + This is a required field. @@ -212,46 +224,50 @@ > - + This is a required field. - + - + This is a required field. - + This is a required field. diff --git a/client/src/vue_components/user/settings/CueColourPreferences.vue b/client/src/vue_components/user/settings/CueColourPreferences.vue index 008f708f..9593e463 100644 --- a/client/src/vue_components/user/settings/CueColourPreferences.vue +++ b/client/src/vue_components/user/settings/CueColourPreferences.vue @@ -53,19 +53,19 @@

Configuration Options

- + This is a required field. @@ -97,19 +97,19 @@

Configuration Options

- + This is a required field. diff --git a/client/src/vue_components/user/settings/StageDirectionStyles.vue b/client/src/vue_components/user/settings/StageDirectionStyles.vue index 559239f6..8f5aceb8 100644 --- a/client/src/vue_components/user/settings/StageDirectionStyles.vue +++ b/client/src/vue_components/user/settings/StageDirectionStyles.vue @@ -54,8 +54,12 @@

Configuration Options

- - + + Default @@ -81,41 +85,41 @@ - + This is a required field. @@ -149,8 +153,12 @@

Configuration Options

- - + + Default @@ -176,41 +184,41 @@ - + This is a required field. From b611dcb36b9ae0ee744fa88121677d0c82e78569 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sat, 31 Jan 2026 00:24:21 +0000 Subject: [PATCH 12/43] Reduce code duplication to pass SonarQube quality gate (#893) * Reduce code duplication to pass SonarQube quality gate Centralize duplicated error messages and form validation code to reduce duplication from 4.87% to below the 3% threshold. Backend changes: - Add controllers/api/constants.py with 40+ standardized error messages - Update 17 controller files to use shared constants - Update 2 test files with improved error message format Frontend changes: - Add mixins/formValidationMixin.js with reusable validation methods - Update 9 Vue components to use the shared mixin Co-Authored-By: Claude Opus 4.5 * Fix Python formatting in tags.py Co-Authored-By: Claude Opus 4.5 * Reduce duplication in stage direction style validation Extract common validation logic into validate_style_fields() helper function to reduce code duplication between post() and patch() methods. Co-Authored-By: Claude Opus 4.5 * Reduce code duplication in stage and override controllers Extract common patterns to reduce SonarQube duplication metrics: - Create helpers.py with shared allocation CRUD logic for props/scenery - Extract handle_override_patch/delete helpers in overrides.py - Update props.py and scenery.py to use shared helpers Net reduction of ~247 lines while preserving all functionality. Co-Authored-By: Claude Opus 4.5 * Extract type controller CRUD helpers to reduce duplication Add handle_type_post, handle_type_patch, handle_type_delete helpers and update PropsTypesController and SceneryTypesController to use them. Co-Authored-By: Claude Opus 4.5 * Simplify form validation mixin API Remove validateNewState and validateEditState convenience wrappers. Components now call getValidationState directly with the form state key. Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- client/src/mixins/formValidationMixin.js | 77 ++++ client/src/views/show/config/ConfigCast.vue | 34 +- .../views/show/config/ConfigCharacters.vue | 38 +- .../config/acts_and_scenes/ConfigActs.vue | 32 +- .../config/acts_and_scenes/ConfigScenes.vue | 44 +- .../config/characters/CharacterGroups.vue | 38 +- .../show/config/stage/CrewList.vue | 30 +- .../show/config/stage/PropsList.vue | 26 +- .../show/config/stage/SceneryList.vue | 26 +- .../user/settings/CueColourPreferences.vue | 28 +- server/controllers/api/constants.py | 101 +++++ server/controllers/api/show/acts.py | 40 +- server/controllers/api/show/cast.py | 36 +- server/controllers/api/show/characters.py | 59 +-- server/controllers/api/show/cues.py | 71 ++-- server/controllers/api/show/microphones.py | 48 ++- server/controllers/api/show/scenes.py | 34 +- .../controllers/api/show/script/compiled.py | 14 +- .../controllers/api/show/script/revisions.py | 30 +- server/controllers/api/show/script/script.py | 13 +- .../api/show/script/stage_direction_styles.py | 130 +++--- .../api/show/session/assign_tags.py | 3 +- .../controllers/api/show/session/sessions.py | 7 +- server/controllers/api/show/session/tags.py | 40 +- server/controllers/api/show/shows.py | 5 +- server/controllers/api/show/stage/crew.py | 35 +- server/controllers/api/show/stage/helpers.py | 393 ++++++++++++++++++ server/controllers/api/show/stage/props.py | 329 ++++----------- server/controllers/api/show/stage/scenery.py | 334 ++++----------- server/controllers/api/user/overrides.py | 271 ++++++------ .../controllers/api/show/stage/test_props.py | 2 +- .../api/show/stage/test_scenery.py | 2 +- 32 files changed, 1252 insertions(+), 1118 deletions(-) create mode 100644 client/src/mixins/formValidationMixin.js create mode 100644 server/controllers/api/constants.py create mode 100644 server/controllers/api/show/stage/helpers.py diff --git a/client/src/mixins/formValidationMixin.js b/client/src/mixins/formValidationMixin.js new file mode 100644 index 00000000..0f955d25 --- /dev/null +++ b/client/src/mixins/formValidationMixin.js @@ -0,0 +1,77 @@ +/** + * Mixin providing common form validation helper methods for Vuelidate. + * + * This mixin centralizes the repeated validation and form reset patterns used + * across multiple components that handle new/edit forms. + * + * Usage: + * 1. Import and include in your component's mixins array + * 2. Define your form state properties (e.g., newFormState, editFormState) + * 3. Define your validations object as usual + * 4. Use the helper methods in your template and component logic + * + * Example: + * import formValidationMixin from '@/mixins/formValidationMixin'; + * + * export default { + * mixins: [formValidationMixin], + * data() { + * return { + * newFormState: { name: '' }, + * editFormState: { id: null, name: '' }, + * }; + * }, + * validations: { + * newFormState: { name: { required } }, + * editFormState: { name: { required } }, + * }, + * methods: { + * resetNewForm() { + * this.resetForm('newFormState', { name: '' }); + * }, + * }, + * }; + */ +export default { + methods: { + /** + * Get Bootstrap validation state for a form field. + * + * @param {string} formStateKey - The validation group key (e.g., 'newFormState', 'editFormState') + * @param {string} fieldName - The field name within the validation group + * @returns {boolean|null} - true if valid, false if invalid, null if pristine + */ + getValidationState(formStateKey, fieldName) { + const field = this.$v[formStateKey][fieldName]; + if (!field) { + return null; + } + const { $dirty, $error } = field; + return $dirty ? !$error : null; + }, + + /** + * Reset a form to its initial state and reset validation. + * + * @param {string} formStateKey - The data property key (e.g., 'newFormState') + * @param {Object} initialState - The initial state object to reset to + */ + resetForm(formStateKey, initialState) { + this[formStateKey] = { ...initialState }; + this.$nextTick(() => { + this.$v.$reset(); + }); + }, + + /** + * Check if a form has validation errors after touching all fields. + * + * @param {string} formStateKey - The validation group key + * @returns {boolean} - true if the form has any validation errors + */ + hasFormErrors(formStateKey) { + this.$v[formStateKey].$touch(); + return this.$v[formStateKey].$anyError; + }, + }, +}; diff --git a/client/src/views/show/config/ConfigCast.vue b/client/src/views/show/config/ConfigCast.vue index 39b0e7f7..ae2de076 100644 --- a/client/src/views/show/config/ConfigCast.vue +++ b/client/src/views/show/config/ConfigCast.vue @@ -71,7 +71,7 @@ id="new-first-name-input" v-model="$v.newFormState.firstName.$model" name="new-first-name-input" - :state="validateNewState('firstName')" + :state="getValidationState('newFormState', 'firstName')" aria-describedby="new-first-name-feedback" /> @@ -87,7 +87,7 @@ id="new-last-name-input" v-model="$v.newFormState.lastName.$model" name="new-last-name-input" - :state="validateNewState('lastName')" + :state="getValidationState('newFormState', 'lastName')" aria-describedby="new-last-name-feedback" /> @@ -115,7 +115,7 @@ id="edit-first-name-input" v-model="$v.editFormState.firstName.$model" name="edit-first-name-input" - :state="validateEditState('firstName')" + :state="getValidationState('editFormState', 'firstName')" aria-describedby="edit-first-name-feedback" /> @@ -131,7 +131,7 @@ id="edit-last-name-input" v-model="$v.editFormState.lastName.$model" name="edit-last-name-input" - :state="validateEditState('lastName')" + :state="getValidationState('editFormState', 'lastName')" aria-describedby="edit-last-name-feedback" /> @@ -148,10 +148,12 @@ import { required } from 'vuelidate/lib/validators'; import { mapGetters, mapActions } from 'vuex'; import CastLineStats from '@/vue_components/show/config/cast/CastLineStats.vue'; import log from 'loglevel'; +import formValidationMixin from '@/mixins/formValidationMixin'; export default { name: 'ConfigCast', components: { CastLineStats }, + mixins: [formValidationMixin], data() { return { castFields: ['first_name', 'last_name', { key: 'btn', label: '' }], @@ -198,15 +200,11 @@ export default { }, methods: { resetNewForm() { - this.newFormState = { + this.resetForm('newFormState', { firstName: '', lastName: '', - }; - this.submittingNewCast = false; - - this.$nextTick(() => { - this.$v.$reset(); }); + this.submittingNewCast = false; }, async onSubmitNew(event) { this.$v.newFormState.$touch(); @@ -227,10 +225,6 @@ export default { this.submittingNewCast = false; } }, - validateNewState(name) { - const { $dirty, $error } = this.$v.newFormState[name]; - return $dirty ? !$error : null; - }, openEditForm(castMember) { if (castMember != null) { this.editFormState.id = castMember.item.id; @@ -241,18 +235,14 @@ export default { } }, resetEditForm() { - this.editFormState = { + this.resetForm('editFormState', { id: null, showID: null, firstName: '', lastName: '', - }; + }); this.submittingEditCast = false; this.deletingCast = false; - - this.$nextTick(() => { - this.$v.$reset(); - }); }, async onSubmitEdit(event) { this.$v.editFormState.$touch(); @@ -273,10 +263,6 @@ export default { this.submittingEditCast = false; } }, - validateEditState(name) { - const { $dirty, $error } = this.$v.editFormState[name]; - return $dirty ? !$error : null; - }, async deleteCastMember(castMember) { if (this.deletingCast) { return; diff --git a/client/src/views/show/config/ConfigCharacters.vue b/client/src/views/show/config/ConfigCharacters.vue index ffe45695..1ce8c5c1 100644 --- a/client/src/views/show/config/ConfigCharacters.vue +++ b/client/src/views/show/config/ConfigCharacters.vue @@ -78,7 +78,7 @@ id="new-name-input" v-model="$v.newFormState.name.$model" name="new-name-input" - :state="validateNewState('name')" + :state="getValidationState('newFormState', 'name')" aria-describedby="new-name-feedback" /> @@ -94,7 +94,7 @@ id="new-description-input" v-model="$v.newFormState.description.$model" name="new-description-input" - :state="validateNewState('description')" + :state="getValidationState('newFormState', 'description')" /> @@ -126,7 +126,7 @@ id="edit-name-input" v-model="$v.editFormState.name.$model" name="edit-name-input" - :state="validateEditState('name')" + :state="getValidationState('editFormState', 'name')" aria-describedby="edit-name-feedback" /> @@ -142,7 +142,7 @@ id="edit-description-input" v-model="$v.editFormState.description.$model" name="edit-description-input" - :state="validateEditState('description')" + :state="getValidationState('editFormState', 'description')" /> @@ -168,10 +168,12 @@ import { mapGetters, mapActions } from 'vuex'; import CharacterLineStats from '@/vue_components/show/config/characters/CharacterLineStats.vue'; import log from 'loglevel'; import CharacterGroups from '@/vue_components/show/config/characters/CharacterGroups.vue'; +import formValidationMixin from '@/mixins/formValidationMixin'; export default { name: 'ConfigCharacters', components: { CharacterGroups, CharacterLineStats }, + mixins: [formValidationMixin], data() { return { rowsPerPage: 15, @@ -232,16 +234,12 @@ export default { }, methods: { resetNewForm() { - this.newFormState = { + this.resetForm('newFormState', { name: '', description: '', played_by: null, - }; - this.submittingNewCharacter = false; - - this.$nextTick(() => { - this.$v.$reset(); }); + this.submittingNewCharacter = false; }, async onSubmitNew(event) { this.$v.newFormState.$touch(); @@ -262,10 +260,6 @@ export default { this.submittingNewCharacter = false; } }, - validateNewState(name) { - const { $dirty, $error } = this.$v.newFormState[name]; - return $dirty ? !$error : null; - }, openEditForm(character) { if (character != null) { this.editFormState.id = character.item.id; @@ -277,19 +271,15 @@ export default { } }, resetEditForm() { - this.editFormState = { + this.resetForm('editFormState', { id: null, showID: null, name: '', description: '', played_by: null, - }; + }); this.submittingEditCharacter = false; this.deletingCharacter = false; - - this.$nextTick(() => { - this.$v.$reset(); - }); }, async onSubmitEdit(event) { this.$v.editFormState.$touch(); @@ -310,10 +300,6 @@ export default { this.submittingEditCharacter = false; } }, - validateEditState(name) { - const { $dirty, $error } = this.$v.editFormState[name]; - return $dirty ? !$error : null; - }, async deleteCharacter(character) { if (this.deletingCharacter) { return; diff --git a/client/src/vue_components/show/config/acts_and_scenes/ConfigActs.vue b/client/src/vue_components/show/config/acts_and_scenes/ConfigActs.vue index e1487819..beda3fd1 100644 --- a/client/src/vue_components/show/config/acts_and_scenes/ConfigActs.vue +++ b/client/src/vue_components/show/config/acts_and_scenes/ConfigActs.vue @@ -72,7 +72,7 @@ id="new-name-input" v-model="$v.newFormState.name.$model" name="new-name-input" - :state="validateNewState('name')" + :state="getValidationState('newFormState', 'name')" aria-describedby="new-name-feedback" /> @@ -119,7 +119,7 @@ id="edit-name-input" v-model="$v.editFormState.name.$model" name="edit-name-input" - :state="validateEditState('name')" + :state="getValidationState('editFormState', 'name')" aria-describedby="edit-name-feedback" /> @@ -146,7 +146,7 @@ id="edit-previous-act-input" v-model="$v.editFormState.previous_act_id.$model" :options="editFormActOptions" - :state="validateEditState('previous_act_id')" + :state="getValidationState('editFormState', 'previous_act_id')" aria-describedby="edit-previous-act-feedback" /> @@ -165,9 +165,11 @@ import { required, integer } from 'vuelidate/lib/validators'; import { mapGetters, mapActions } from 'vuex'; import log from 'loglevel'; +import formValidationMixin from '@/mixins/formValidationMixin'; export default { name: 'ConfigActs', + mixins: [formValidationMixin], data() { return { loading: true, @@ -271,20 +273,12 @@ export default { }, methods: { resetNewForm() { - this.newFormState = { + this.resetForm('newFormState', { name: '', interval_after: false, previous_act_id: null, - }; - this.submittingNewAct = false; - - this.$nextTick(() => { - this.$v.$reset(); }); - }, - validateNewState(name) { - const { $dirty, $error } = this.$v.newFormState[name]; - return $dirty ? !$error : null; + this.submittingNewAct = false; }, async onSubmitNew(event) { this.$v.newFormState.$touch(); @@ -318,19 +312,15 @@ export default { } }, resetEditForm() { - this.editFormState = { + this.resetForm('editFormState', { id: null, showID: null, name: '', interval_after: false, previous_act_id: null, - }; + }); this.submittingEditAct = false; this.deletingAct = false; - - this.$nextTick(() => { - this.$v.$reset(); - }); }, async onSubmitEdit(event) { this.$v.editFormState.$touch(); @@ -351,10 +341,6 @@ export default { this.submittingEditAct = false; } }, - validateEditState(name) { - const { $dirty, $error } = this.$v.editFormState[name]; - return $dirty ? !$error : null; - }, async deleteAct(act) { if (this.deletingAct) { return; diff --git a/client/src/vue_components/show/config/acts_and_scenes/ConfigScenes.vue b/client/src/vue_components/show/config/acts_and_scenes/ConfigScenes.vue index 78d1cc18..9b46ea37 100644 --- a/client/src/vue_components/show/config/acts_and_scenes/ConfigScenes.vue +++ b/client/src/vue_components/show/config/acts_and_scenes/ConfigScenes.vue @@ -96,7 +96,7 @@ id="new-name-input" v-model="$v.newFormState.name.$model" name="new-name-input" - :state="validateNewState('name')" + :state="getValidationState('newFormState', 'name')" aria-describedby="new-name-feedback" /> @@ -108,7 +108,7 @@ id="new-act-input" v-model="$v.newFormState.act_id.$model" :options="actOptions" - :state="validateNewState('act_id')" + :state="getValidationState('newFormState', 'act_id')" aria-describedby="new-act-feedback" /> @@ -144,7 +144,7 @@ id="edit-name-input" v-model="$v.editFormState.name.$model" name="edit-name-input" - :state="validateEditState('name')" + :state="getValidationState('editFormState', 'name')" aria-describedby="edit-name-feedback" /> @@ -156,7 +156,7 @@ id="edit-act-input" v-model="$v.editFormState.act_id.$model" :options="actOptions" - :state="validateEditState('act_id')" + :state="getValidationState('editFormState', 'act_id')" aria-describedby="edit-act-feedback" @change="editActChanged" /> @@ -173,7 +173,7 @@ id="edit-previous-scene-input" v-model="$v.editFormState.previous_scene_id.$model" :options="editFormPrevScenes" - :state="validateEditState('previous_scene_id')" + :state="getValidationState('editFormState', 'previous_scene_id')" aria-describedby="edit-previous-scene-feedback" /> @@ -215,9 +215,11 @@ import { required, integer } from 'vuelidate/lib/validators'; import { mapGetters, mapActions } from 'vuex'; import log from 'loglevel'; +import formValidationMixin from '@/mixins/formValidationMixin'; export default { name: 'ConfigScenes', + mixins: [formValidationMixin], data() { return { loading: true, @@ -420,20 +422,12 @@ export default { }, methods: { resetNewForm() { - this.newFormState = { + this.resetForm('newFormState', { name: '', act_id: null, previous_scene_id: null, - }; - this.submittingNewScene = false; - - this.$nextTick(() => { - this.$v.$reset(); }); - }, - validateNewState(name) { - const { $dirty, $error } = this.$v.newFormState[name]; - return $dirty ? !$error : null; + this.submittingNewScene = false; }, async onSubmitNew(event) { this.$v.newFormState.$touch(); @@ -473,15 +467,11 @@ export default { } }, resetFirstSceneForm() { - this.firstSceneFormState = { + this.resetForm('firstSceneFormState', { act_id: null, scene_id: null, - }; - this.submittingFirstScene = false; - - this.$nextTick(() => { - this.$v.$reset(); }); + this.submittingFirstScene = false; }, openFirstSceneEdit(act) { if (act != null) { @@ -513,18 +503,14 @@ export default { }, resetEditForm() { this.editSceneID = null; - this.editFormState = { + this.resetForm('editFormState', { scene_id: null, name: '', act_id: null, previous_scene_id: null, - }; + }); this.submittingEditScene = false; this.deletingScene = false; - - this.$nextTick(() => { - this.$v.$reset(); - }); }, openEditForm(scene) { if (scene != null) { @@ -540,10 +526,6 @@ export default { this.$bvModal.show('edit-scene'); } }, - validateEditState(name) { - const { $dirty, $error } = this.$v.editFormState[name]; - return $dirty ? !$error : null; - }, async onSubmitEdit(event) { this.$v.editFormState.$touch(); if (this.$v.editFormState.$anyError || this.submittingEditScene) { diff --git a/client/src/vue_components/show/config/characters/CharacterGroups.vue b/client/src/vue_components/show/config/characters/CharacterGroups.vue index 1788981a..a199364b 100644 --- a/client/src/vue_components/show/config/characters/CharacterGroups.vue +++ b/client/src/vue_components/show/config/characters/CharacterGroups.vue @@ -67,7 +67,7 @@ id="new-name-input" v-model="$v.newFormState.name.$model" name="new-name-input" - :state="validateNewState('name')" + :state="getValidationState('newFormState', 'name')" aria-describedby="new-name-feedback" /> @@ -83,7 +83,7 @@ id="new-description-input" v-model="$v.newFormState.description.$model" name="new-description-input" - :state="validateNewState('description')" + :state="getValidationState('newFormState', 'description')" /> @@ -120,7 +120,7 @@ id="edit-name-input" v-model="$v.editFormState.name.$model" name="edit-name-input" - :state="validateEditState('name')" + :state="getValidationState('editFormState', 'name')" aria-describedby="edit-name-feedback" /> @@ -136,7 +136,7 @@ id="edit-description-input" v-model="$v.editFormState.description.$model" name="edit-description-input" - :state="validateEditState('description')" + :state="getValidationState('editFormState', 'description')" /> @@ -168,9 +168,11 @@ import { mapActions, mapGetters } from 'vuex'; import { required } from 'vuelidate/lib/validators'; import log from 'loglevel'; +import formValidationMixin from '@/mixins/formValidationMixin'; export default { name: 'CharacterGroups', + mixins: [formValidationMixin], data() { return { loading: true, @@ -224,16 +226,12 @@ export default { }, resetNewForm() { this.tempCharacterList = []; - this.newFormState = { + this.resetForm('newFormState', { name: '', description: '', characters: [], - }; - this.submittingNewGroup = false; - - this.$nextTick(() => { - this.$v.$reset(); }); + this.submittingNewGroup = false; }, async onSubmitNew(event) { this.$v.newFormState.$touch(); @@ -254,10 +252,6 @@ export default { this.submittingNewGroup = false; } }, - validateNewState(name) { - const { $dirty, $error } = this.$v.newFormState[name]; - return $dirty ? !$error : null; - }, async deleteCharacterGroup(characterGroup) { if (this.deletingGroup) { return; @@ -293,19 +287,15 @@ export default { } }, resetEditForm() { - this.editFormState = { + this.resetForm('editFormState', { id: null, name: '', description: '', characters: [], - }; + }); this.tempEditCharacterList = []; this.submittingEditGroup = false; this.deletingGroup = false; - - this.$nextTick(() => { - this.$v.$reset(); - }); }, editSelectChanged(value, id) { this.$v.editFormState.characters.$model = value.map((character) => character.id); @@ -329,10 +319,6 @@ export default { this.submittingEditGroup = false; } }, - validateEditState(name) { - const { $dirty, $error } = this.$v.editFormState[name]; - return $dirty ? !$error : null; - }, ...mapActions([ 'GET_CHARACTER_LIST', 'GET_CHARACTER_GROUP_LIST', diff --git a/client/src/vue_components/show/config/stage/CrewList.vue b/client/src/vue_components/show/config/stage/CrewList.vue index 0a5388c6..782cc523 100644 --- a/client/src/vue_components/show/config/stage/CrewList.vue +++ b/client/src/vue_components/show/config/stage/CrewList.vue @@ -47,7 +47,7 @@ id="new-first-name-input" v-model="$v.newFormState.firstName.$model" name="new-first-name-input" - :state="validateNewState('firstName')" + :state="getValidationState('newFormState', 'firstName')" aria-describedby="new-first-name-feedback" /> @@ -63,7 +63,7 @@ id="new-last-name-input" v-model="$v.newFormState.lastName.$model" name="new-last-name-input" - :state="validateNewState('lastName')" + :state="getValidationState('newFormState', 'lastName')" aria-describedby="new-last-name-feedback" /> @@ -90,7 +90,7 @@ id="edit-first-name-input" v-model="$v.editFormState.firstName.$model" name="edit-first-name-input" - :state="validateEditState('firstName')" + :state="getValidationState('editFormState', 'firstName')" aria-describedby="edit-first-name-feedback" /> @@ -106,7 +106,7 @@ id="edit-last-name-input" v-model="$v.editFormState.lastName.$model" name="edit-last-name-input" - :state="validateEditState('lastName')" + :state="getValidationState('editFormState', 'lastName')" aria-describedby="edit-last-name-feedback" /> @@ -121,9 +121,11 @@ diff --git a/client/src/vue_components/show/config/mics/MicTimeline.vue b/client/src/vue_components/show/config/mics/MicTimeline.vue index 67d487b7..95980978 100644 --- a/client/src/vue_components/show/config/mics/MicTimeline.vue +++ b/client/src/vue_components/show/config/mics/MicTimeline.vue @@ -1,5 +1,5 @@ + + + + diff --git a/client/src/vue_components/show/live/StageManagerPane.vue b/client/src/vue_components/show/live/StageManagerPane.vue index 107975b5..70425282 100644 --- a/client/src/vue_components/show/live/StageManagerPane.vue +++ b/client/src/vue_components/show/live/StageManagerPane.vue @@ -49,25 +49,6 @@ - -
- -
    -
  • - {{ item.name }} -
  • -
-
- -
- -
    -
  • - {{ item.name }} -
  • -
-
-
+ Plan +
+
+ +
    +
  • + {{ getSceneryDisplayName(item) }} +
  • +
+
+
+ +
    +
  • + {{ getPropDisplayName(item) }} +
  • +
+
+
+ +
+ + +
+ + + Setting + +
+
+ + + + + + Scenery + + + Props + + + + +
    +
  • + {{ getSceneryDisplayName(item) }} +
  • +
+

None

+
+ +
    +
  • + {{ getPropDisplayName(item) }} +
  • +
+

None

+
+
+
+
+
+
+ + +
+ + + Striking + +
+
+ + + + + + Scenery + + + Props + + + + +
    +
  • + {{ getSceneryDisplayName(item) }} +
  • +
+

None

+
+ +
    +
  • + {{ getPropDisplayName(item) }} +
  • +
+

None

+
+
+
+
+
+
+
+
+

No scene selected.

+
+
@@ -102,6 +213,9 @@ export default { expandedScenes: {}, pinnedScenes: {}, debounceContentSize: null, + smPlanScene: null, + smPlanSet: true, + smPlanStrike: true, }; }, computed: { @@ -157,6 +271,8 @@ export default { 'SCENERY_ALLOCATIONS', 'PROPS_LIST', 'SCENERY_LIST', + 'PROP_TYPES_DICT', + 'SCENERY_TYPES_DICT', ]), }, watch: { @@ -213,6 +329,8 @@ export default { this.GET_SCENERY_LIST(), this.GET_PROPS_ALLOCATIONS(), this.GET_SCENERY_ALLOCATIONS(), + this.GET_PROP_TYPES(), + this.GET_SCENERY_TYPES(), ]); this.loaded = true; @@ -267,6 +385,14 @@ export default { .map((alloc) => this.sceneryDict[alloc.scenery_id]) .filter((scenery) => scenery != null); }, + getPropDisplayName(prop) { + const propType = this.PROP_TYPES_DICT[prop.prop_type_id]; + return propType ? `${propType.name}: ${prop.name}` : prop.name; + }, + getSceneryDisplayName(scenery) { + const sceneryType = this.SCENERY_TYPES_DICT[scenery.scenery_type_id]; + return sceneryType ? `${sceneryType.name}: ${scenery.name}` : scenery.name; + }, autoScrollToCurrentScene(currentSceneId) { const container = this.$refs.scrollContainer; if (!container) return; @@ -295,6 +421,66 @@ export default { container.scrollTo({ top: scrollOffset, behavior: 'smooth' }); } }, + resetSMPlanScene() { + this.smPlanScene = null; + }, + showSMPlanModal(scene) { + this.smPlanScene = scene; + this.$bvModal.show('sm-plan-modal'); + }, + togglePlanSet() { + this.smPlanSet = !this.smPlanSet; + }, + togglePlanStrike() { + this.smPlanStrike = !this.smPlanStrike; + }, + getPreviousScene(scene) { + const currentIndex = this.orderedScenes.findIndex((s) => s.id === scene.id); + if (currentIndex <= 0) { + return null; // First scene or not found + } + return this.orderedScenes[currentIndex - 1]; + }, + getSettingScenery(scene) { + const currentScenery = this.getSceneryForScene(scene.id); + const previousScene = this.getPreviousScene(scene); + if (!previousScene) { + return currentScenery; // First scene - all items are being set + } + const previousSceneryIds = new Set( + this.getSceneryForScene(previousScene.id).map((s) => s.id) + ); + return currentScenery.filter((item) => !previousSceneryIds.has(item.id)); + }, + getSettingProps(scene) { + const currentProps = this.getPropsForScene(scene.id); + const previousScene = this.getPreviousScene(scene); + if (!previousScene) { + return currentProps; // First scene - all items are being set + } + const previousPropsIds = new Set(this.getPropsForScene(previousScene.id).map((p) => p.id)); + return currentProps.filter((item) => !previousPropsIds.has(item.id)); + }, + getStrikingScenery(scene) { + const previousScene = this.getPreviousScene(scene); + if (!previousScene) { + return []; // First scene - nothing to strike + } + const currentSceneryIds = new Set(this.getSceneryForScene(scene.id).map((s) => s.id)); + return this.getSceneryForScene(previousScene.id).filter( + (item) => !currentSceneryIds.has(item.id) + ); + }, + getStrikingProps(scene) { + const previousScene = this.getPreviousScene(scene); + if (!previousScene) { + return []; // First scene - nothing to strike + } + const currentPropsIds = new Set(this.getPropsForScene(scene.id).map((p) => p.id)); + return this.getPropsForScene(previousScene.id).filter( + (item) => !currentPropsIds.has(item.id) + ); + }, ...mapActions([ 'GET_ACT_LIST', 'GET_SCENE_LIST', @@ -302,6 +488,8 @@ export default { 'GET_SCENERY_LIST', 'GET_PROPS_ALLOCATIONS', 'GET_SCENERY_ALLOCATIONS', + 'GET_PROP_TYPES', + 'GET_SCENERY_TYPES', ]), }, }; @@ -431,4 +619,31 @@ export default { color: #6c757d; font-style: italic; } + +.plan-header-row { + margin: 0; + padding: 0; + border-bottom: 1px solid #dee2e6; +} + +.plan-header-col { + padding: 0.5rem 0.75rem; +} + +.plan-header-col.border-right { + border-right: 1px solid #dee2e6; +} + +.plan-content-row { + margin: 0; + padding: 0; +} + +.plan-content-col { + padding: 0.5rem 0.75rem; +} + +.plan-content-col.border-right { + border-right: 1px solid #dee2e6; +} From fca6d7b14173d50f679f517c01f6b33e6f69bb28 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sun, 1 Feb 2026 00:44:25 +0000 Subject: [PATCH 15/43] [npm] Update packages --- client/package-lock.json | 188 +++++++++++++++++++-------------------- client/package.json | 4 +- 2 files changed, 96 insertions(+), 96 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 629e1457..07b83654 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -34,9 +34,9 @@ "vuex-persistedstate": "3.2.1" }, "devDependencies": { - "@babel/core": "7.28.6", + "@babel/core": "7.29.0", "@babel/eslint-parser": "7.28.6", - "@babel/preset-env": "7.28.6", + "@babel/preset-env": "7.29.0", "@eslint/js": "^9.39.2", "@types/vuelidate": "^0.7.22", "@vitejs/plugin-vue2": "2.3.4", @@ -121,9 +121,9 @@ "license": "MIT" }, "node_modules/@babel/code-frame": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", - "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "dependencies": { @@ -136,9 +136,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", - "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "dev": true, "license": "MIT", "engines": { @@ -146,21 +146,21 @@ } }, "node_modules/@babel/core": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", - "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.28.6", + "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -196,14 +196,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", - "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", + "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -283,17 +283,17 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", - "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.6.tgz", + "integrity": "sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "debug": "^4.4.1", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", "lodash.debounce": "^4.0.8", - "resolve": "^1.22.10" + "resolve": "^1.22.11" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -486,12 +486,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", - "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -663,15 +663,15 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.6.tgz", - "integrity": "sha512-9knsChgsMzBV5Yh3kkhrZNxH3oCYAfMBkNNaVN4cP2RVlFPe8wYdwwcnOsAbkdDoV9UjFtOXWrWB52M8W4jNeA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", + "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.28.6" + "@babel/traverse": "^7.29.0" }, "engines": { "node": ">=6.9.0" @@ -853,9 +853,9 @@ } }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.28.6.tgz", - "integrity": "sha512-5suVoXjC14lUN6ZL9OLKIHCNVWCrqGqlmEp/ixdXjvgnEl/kauLvvMO/Xw9NyMc95Joj1AeLVPVMvibBgSoFlA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", "dev": true, "license": "MIT", "dependencies": { @@ -1068,16 +1068,16 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", - "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", + "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.5" + "@babel/traverse": "^7.29.0" }, "engines": { "node": ">=6.9.0" @@ -1104,14 +1104,14 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1306,9 +1306,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.6.tgz", - "integrity": "sha512-eZhoEZHYQLL5uc1gS5e9/oTknS0sSSAtd5TkKMUp3J+S/CaUjagc0kOUPsEbDmMeva0nC3WWl4SxVY6+OBuxfw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", + "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", "dev": true, "license": "MIT", "dependencies": { @@ -1503,13 +1503,13 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.6.tgz", - "integrity": "sha512-GaTI4nXDrs7l0qaJ6Rg06dtOXTBCG6TMDB44zbqofCIC4PqC7SEvmFFtpxzCDw9W5aJ7RKVshgXTLvLdBFV/qw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.0.tgz", + "integrity": "sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", + "@babel/compat-data": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", @@ -1523,7 +1523,7 @@ "@babel/plugin-syntax-import-attributes": "^7.28.6", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.28.6", + "@babel/plugin-transform-async-generator-functions": "^7.29.0", "@babel/plugin-transform-async-to-generator": "^7.28.6", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", "@babel/plugin-transform-block-scoping": "^7.28.6", @@ -1534,7 +1534,7 @@ "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-dotall-regex": "^7.28.6", "@babel/plugin-transform-duplicate-keys": "^7.27.1", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.28.6", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", "@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-explicit-resource-management": "^7.28.6", "@babel/plugin-transform-exponentiation-operator": "^7.28.6", @@ -1547,9 +1547,9 @@ "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.28.6", - "@babel/plugin-transform-modules-systemjs": "^7.28.5", + "@babel/plugin-transform-modules-systemjs": "^7.29.0", "@babel/plugin-transform-modules-umd": "^7.27.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", "@babel/plugin-transform-numeric-separator": "^7.28.6", @@ -1561,7 +1561,7 @@ "@babel/plugin-transform-private-methods": "^7.28.6", "@babel/plugin-transform-private-property-in-object": "^7.28.6", "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.6", + "@babel/plugin-transform-regenerator": "^7.29.0", "@babel/plugin-transform-regexp-modifiers": "^7.28.6", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", @@ -1574,10 +1574,10 @@ "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.14", - "babel-plugin-polyfill-corejs3": "^0.13.0", - "babel-plugin-polyfill-regenerator": "^0.6.5", - "core-js-compat": "^3.43.0", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", "semver": "^6.3.1" }, "engines": { @@ -1618,18 +1618,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", - "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.6", + "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -1637,9 +1637,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", - "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -3677,14 +3677,14 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", - "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz", + "integrity": "sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.7", - "@babel/helper-define-polyfill-provider": "^0.6.5", + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.6", "semver": "^6.3.1" }, "peerDependencies": { @@ -3692,27 +3692,27 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", - "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.0.tgz", + "integrity": "sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5", - "core-js-compat": "^3.43.0" + "@babel/helper-define-polyfill-provider": "^0.6.6", + "core-js-compat": "^3.48.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", - "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.6.tgz", + "integrity": "sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5" + "@babel/helper-define-polyfill-provider": "^0.6.6" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -4005,13 +4005,13 @@ } }, "node_modules/core-js-compat": { - "version": "3.47.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz", - "integrity": "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==", + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz", + "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.28.0" + "browserslist": "^4.28.1" }, "funding": { "type": "opencollective", diff --git a/client/package.json b/client/package.json index 45e612e0..5893a446 100644 --- a/client/package.json +++ b/client/package.json @@ -52,9 +52,9 @@ "vuex-persistedstate": "3.2.1" }, "devDependencies": { - "@babel/core": "7.28.6", + "@babel/core": "7.29.0", "@babel/eslint-parser": "7.28.6", - "@babel/preset-env": "7.28.6", + "@babel/preset-env": "7.29.0", "@eslint/js": "^9.39.2", "@types/vuelidate": "^0.7.22", "@vitejs/plugin-vue2": "2.3.4", From 530c8398764dbd33aca07277e1b0c3399cc14ba1 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sun, 1 Feb 2026 01:01:46 +0000 Subject: [PATCH 16/43] Always show Plan button in stage manager panel --- .../vue_components/show/live/StageManagerPane.vue | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/src/vue_components/show/live/StageManagerPane.vue b/client/src/vue_components/show/live/StageManagerPane.vue index 70425282..a91467c8 100644 --- a/client/src/vue_components/show/live/StageManagerPane.vue +++ b/client/src/vue_components/show/live/StageManagerPane.vue @@ -49,6 +49,12 @@ +
+ Plan +
- Plan -
- - + + -
Scenery
- - - - - - -
- -
Props
- - - - - - -
-
+
+ + Allocations + + {{ currentSceneSceneryAllocations.length + currentScenePropsAllocations.length }} + + + + +
+ + + +
+ + Scenery + Prop + +
+ + +
Scenery
+ + + + + + +
+ +
Props
+ + + + + + +
+
+
+
+ + + + + +
+ + SET + {{ setItems.length }} + + + + {{ unassignedSetCount }} unassigned + + + + +
+
+ + +
+
+
+ {{ item.name }} + {{ item.itemType }} +
+
+ + {{ formatCrewName(CREW_MEMBER_BY_ID(assignment.crew_id)) }} + + + + + + No crew assigned + +
+
+ + + + + Add + +
+
+
+
+
+
+ + + + +
+ + STRIKE + {{ strikeItems.length }} + + + + {{ unassignedStrikeCount }} unassigned + + + + +
+
+ + +
+
+
+ {{ item.name }} + {{ item.itemType }} +
+
+ + {{ formatCrewName(CREW_MEMBER_BY_ID(assignment.crew_id)) }} + + + + + + No crew assigned + +
+
+ + + + + Add + +
+
+
+
+
+
({ value: p.id, text: p.name })), })).filter((group) => group.options.length > 0); }, + previousSceneInAct() { + if (this.currentSceneIndex <= 0 || !this.currentScene) return null; + const prev = this.orderedScenes[this.currentSceneIndex - 1]; + return prev && prev.act === this.currentScene.act ? prev : null; + }, + nextSceneInAct() { + if (!this.currentScene || this.currentSceneIndex >= this.orderedScenes.length - 1) + return null; + const next = this.orderedScenes[this.currentSceneIndex + 1]; + return next && next.act === this.currentScene.act ? next : null; + }, + setItems() { + if (!this.currentScene) return []; + const items = []; + const sceneId = this.currentScene.id; + const prevSceneId = this.previousSceneInAct?.id; + + for (const scenery of this.SCENERY_LIST) { + const allocs = this.SCENERY_ALLOCATIONS_BY_ITEM[scenery.id] || []; + const inCurrent = allocs.some((a) => a.scene_id === sceneId); + if (!inCurrent) continue; + const inPrev = prevSceneId && allocs.some((a) => a.scene_id === prevSceneId); + if (!inPrev) { + items.push({ itemId: scenery.id, itemType: 'scenery', name: scenery.name }); + } + } + + for (const prop of this.PROPS_LIST) { + const allocs = this.PROPS_ALLOCATIONS_BY_ITEM[prop.id] || []; + const inCurrent = allocs.some((a) => a.scene_id === sceneId); + if (!inCurrent) continue; + const inPrev = prevSceneId && allocs.some((a) => a.scene_id === prevSceneId); + if (!inPrev) { + items.push({ itemId: prop.id, itemType: 'prop', name: prop.name }); + } + } + + return items; + }, + strikeItems() { + if (!this.currentScene) return []; + const items = []; + const sceneId = this.currentScene.id; + const nextSceneId = this.nextSceneInAct?.id; + + for (const scenery of this.SCENERY_LIST) { + const allocs = this.SCENERY_ALLOCATIONS_BY_ITEM[scenery.id] || []; + const inCurrent = allocs.some((a) => a.scene_id === sceneId); + if (!inCurrent) continue; + const inNext = nextSceneId && allocs.some((a) => a.scene_id === nextSceneId); + if (!inNext) { + items.push({ itemId: scenery.id, itemType: 'scenery', name: scenery.name }); + } + } + + for (const prop of this.PROPS_LIST) { + const allocs = this.PROPS_ALLOCATIONS_BY_ITEM[prop.id] || []; + const inCurrent = allocs.some((a) => a.scene_id === sceneId); + if (!inCurrent) continue; + const inNext = nextSceneId && allocs.some((a) => a.scene_id === nextSceneId); + if (!inNext) { + items.push({ itemId: prop.id, itemType: 'prop', name: prop.name }); + } + } + + return items; + }, + unassignedSetCount() { + return this.setItems.filter( + (item) => this.getAssignmentsForItem(item.itemId, item.itemType, 'set').length === 0 + ).length; + }, + unassignedStrikeCount() { + return this.strikeItems.filter( + (item) => this.getAssignmentsForItem(item.itemId, item.itemType, 'strike').length === 0 + ).length; + }, currentSceneSceneryAllocations() { if (!this.currentScene) return []; return this.SCENERY_ALLOCATIONS.filter((a) => a.scene_id === this.currentScene.id); @@ -353,10 +651,16 @@ export default { 'SCENERY_TYPES', 'SCENERY_TYPE_BY_ID', 'SCENERY_ALLOCATIONS', + 'SCENERY_ALLOCATIONS_BY_ITEM', 'PROPS_LIST', 'PROP_TYPES', 'PROP_TYPE_BY_ID', 'PROPS_ALLOCATIONS', + 'PROPS_ALLOCATIONS_BY_ITEM', + 'CREW_LIST', + 'CREW_MEMBER_BY_ID', + 'CREW_ASSIGNMENTS_BY_PROP', + 'CREW_ASSIGNMENTS_BY_SCENERY', ]), }, async mounted() { @@ -369,6 +673,8 @@ export default { this.GET_PROP_TYPES(), this.GET_PROPS_LIST(), this.GET_PROPS_ALLOCATIONS(), + this.GET_CREW_LIST(), + this.GET_CREW_ASSIGNMENTS(), ]); this.loaded = true; this.calculateNavbarHeight(); @@ -435,11 +741,30 @@ export default { event.preventDefault(); return; } - await this.ADD_SCENERY_ALLOCATION({ - scenery_id: this.addSceneryFormState.scenery_id, - scene_id: this.currentScene.id, - }); - this.resetAddSceneryForm(); + const sceneryId = this.addSceneryFormState.scenery_id; + const orphans = this.findOrphansForItem(sceneryId, 'scenery', 'add', this.currentScene.id); + if (orphans.length > 0) { + event.preventDefault(); + const msgVNode = this.buildOrphanWarningVNode(orphans); + const confirmed = await this.$bvModal.msgBoxConfirm([msgVNode], { + title: 'Crew assignments will be removed', + okTitle: 'Continue', + okVariant: 'warning', + }); + if (confirmed !== true) return; + await this.ADD_SCENERY_ALLOCATION({ + scenery_id: sceneryId, + scene_id: this.currentScene.id, + }); + this.resetAddSceneryForm(); + this.$bvModal.hide('add-scenery'); + } else { + await this.ADD_SCENERY_ALLOCATION({ + scenery_id: sceneryId, + scene_id: this.currentScene.id, + }); + this.resetAddSceneryForm(); + } }, resetAddPropForm() { this.addPropFormState = { props_id: null }; @@ -457,11 +782,99 @@ export default { event.preventDefault(); return; } - await this.ADD_PROPS_ALLOCATION({ - props_id: this.addPropFormState.props_id, - scene_id: this.currentScene.id, - }); - this.resetAddPropForm(); + const propsId = this.addPropFormState.props_id; + const orphans = this.findOrphansForItem(propsId, 'prop', 'add', this.currentScene.id); + if (orphans.length > 0) { + event.preventDefault(); + const msgVNode = this.buildOrphanWarningVNode(orphans); + const confirmed = await this.$bvModal.msgBoxConfirm([msgVNode], { + title: 'Crew assignments will be removed', + okTitle: 'Continue', + okVariant: 'warning', + }); + if (confirmed !== true) return; + await this.ADD_PROPS_ALLOCATION({ + props_id: propsId, + scene_id: this.currentScene.id, + }); + this.resetAddPropForm(); + this.$bvModal.hide('add-prop'); + } else { + await this.ADD_PROPS_ALLOCATION({ + props_id: propsId, + scene_id: this.currentScene.id, + }); + this.resetAddPropForm(); + } + }, + getAssignmentsForItem(itemId, itemType, assignmentType) { + const assignments = + itemType === 'prop' + ? this.CREW_ASSIGNMENTS_BY_PROP[itemId] || [] + : this.CREW_ASSIGNMENTS_BY_SCENERY[itemId] || []; + return assignments.filter( + (a) => a.assignment_type === assignmentType && a.scene_id === this.currentScene?.id + ); + }, + getAvailableCrewForItem(itemId, itemType, assignmentType) { + const assigned = new Set( + this.getAssignmentsForItem(itemId, itemType, assignmentType).map((a) => a.crew_id) + ); + return this.CREW_LIST.filter((c) => !assigned.has(c.id)).map((c) => ({ + value: c.id, + text: this.formatCrewName(c), + })); + }, + formatCrewName(crew) { + if (!crew) return 'Unknown'; + return crew.last_name ? `${crew.first_name} ${crew.last_name}` : crew.first_name; + }, + crewSelectionKey(itemId, itemType, assignmentType) { + return `${itemType}-${itemId}-${assignmentType}`; + }, + async addCrewAssignment(itemId, itemType, assignmentType) { + const key = this.crewSelectionKey(itemId, itemType, assignmentType); + const crewId = this.newCrewSelections[key]; + if (!crewId || !this.currentScene || this.savingAssignment) return; + + this.savingAssignment = true; + try { + const assignment = { + crew_id: crewId, + scene_id: this.currentScene.id, + assignment_type: assignmentType, + }; + if (itemType === 'prop') { + assignment.prop_id = itemId; + } else { + assignment.scenery_id = itemId; + } + + const result = await this.ADD_CREW_ASSIGNMENT(assignment); + if (result.success) { + this.$set(this.newCrewSelections, key, null); + } + } finally { + this.savingAssignment = false; + } + }, + async removeCrewAssignment(assignment) { + if (this.savingAssignment) return; + + const crew = this.CREW_MEMBER_BY_ID(assignment.crew_id); + const crewName = this.formatCrewName(crew); + const confirmed = await this.$bvModal.msgBoxConfirm( + `Remove ${crewName} from this ${assignment.assignment_type.toUpperCase()} assignment?`, + { okTitle: 'Remove', okVariant: 'danger' } + ); + if (confirmed) { + this.savingAssignment = true; + try { + await this.DELETE_CREW_ASSIGNMENT(assignment.id); + } finally { + this.savingAssignment = false; + } + } }, getSceneryById(id) { return this.SCENERY_LIST.find((s) => s.id === id); @@ -469,21 +882,98 @@ export default { getPropById(id) { return this.PROPS_LIST.find((p) => p.id === id); }, + getItemAllocationsForScenery(sceneryId) { + return this.SCENERY_ALLOCATIONS.filter((a) => a.scenery_id === sceneryId); + }, + getItemAllocationsForProp(propId) { + return this.PROPS_ALLOCATIONS.filter((a) => a.props_id === propId); + }, + findOrphansForItem(itemId, itemType, changeType, sceneId) { + const allocations = + itemType === 'scenery' + ? this.getItemAllocationsForScenery(itemId) + : this.getItemAllocationsForProp(itemId); + const crewAssignments = + itemType === 'scenery' + ? this.CREW_ASSIGNMENTS_BY_SCENERY[itemId] || [] + : this.CREW_ASSIGNMENTS_BY_PROP[itemId] || []; + return findOrphanedAssignments({ + orderedScenes: this.orderedScenes, + currentAllocations: allocations, + crewAssignments, + changeType, + changeSceneId: sceneId, + }); + }, + buildOrphanWarningVNode(orphanedAssignments) { + const h = this.$createElement; + const groups = {}; + for (const assignment of orphanedAssignments) { + const itemName = + assignment.prop_id != null + ? this.getPropById(assignment.prop_id)?.name || 'Unknown Prop' + : this.getSceneryById(assignment.scenery_id)?.name || 'Unknown Scenery'; + const scene = this.orderedScenes.find((s) => s.id === assignment.scene_id); + const sceneName = scene?.name || 'Unknown Scene'; + const key = `${itemName} - ${assignment.assignment_type.toUpperCase()} (${sceneName})`; + if (!groups[key]) groups[key] = []; + const crew = this.CREW_MEMBER_BY_ID(assignment.crew_id); + groups[key].push(this.formatCrewName(crew)); + } + const items = Object.entries(groups).map(([label, names]) => + h('li', {}, `${label}: ${names.join(', ')}`) + ); + return h('div', {}, [ + h('p', {}, 'This action will remove the following crew assignments:'), + h('ul', { class: 'mb-2' }, items), + h('p', { class: 'text-muted mb-0' }, 'You can reassign crew after the change.'), + ]); + }, async deleteSceneryAllocation(allocation) { const scenery = this.getSceneryById(allocation.scenery_id); - const msg = `Remove "${scenery?.name}" from this scene?`; - const action = await this.$bvModal.msgBoxConfirm(msg, {}); - if (action === true) { - await this.DELETE_SCENERY_ALLOCATION(allocation.id); + const orphans = this.findOrphansForItem( + allocation.scenery_id, + 'scenery', + 'remove', + this.currentScene.id + ); + if (orphans.length > 0) { + const msgVNode = this.buildOrphanWarningVNode(orphans); + const action = await this.$bvModal.msgBoxConfirm([msgVNode], { + title: 'Crew assignments will be removed', + okTitle: 'Continue', + okVariant: 'danger', + }); + if (action !== true) return; + } else { + const msg = `Remove "${scenery?.name}" from this scene?`; + const action = await this.$bvModal.msgBoxConfirm(msg, {}); + if (action !== true) return; } + await this.DELETE_SCENERY_ALLOCATION(allocation.id); }, async deletePropAllocation(allocation) { const prop = this.getPropById(allocation.props_id); - const msg = `Remove "${prop?.name}" from this scene?`; - const action = await this.$bvModal.msgBoxConfirm(msg, {}); - if (action === true) { - await this.DELETE_PROPS_ALLOCATION(allocation.id); + const orphans = this.findOrphansForItem( + allocation.props_id, + 'prop', + 'remove', + this.currentScene.id + ); + if (orphans.length > 0) { + const msgVNode = this.buildOrphanWarningVNode(orphans); + const action = await this.$bvModal.msgBoxConfirm([msgVNode], { + title: 'Crew assignments will be removed', + okTitle: 'Continue', + okVariant: 'danger', + }); + if (action !== true) return; + } else { + const msg = `Remove "${prop?.name}" from this scene?`; + const action = await this.$bvModal.msgBoxConfirm(msg, {}); + if (action !== true) return; } + await this.DELETE_PROPS_ALLOCATION(allocation.id); }, ...mapActions([ 'GET_ACT_LIST', @@ -498,6 +988,10 @@ export default { 'GET_PROPS_ALLOCATIONS', 'ADD_PROPS_ALLOCATION', 'DELETE_PROPS_ALLOCATION', + 'GET_CREW_LIST', + 'GET_CREW_ASSIGNMENTS', + 'ADD_CREW_ASSIGNMENT', + 'DELETE_CREW_ASSIGNMENT', ]), }, }; @@ -515,4 +1009,59 @@ export default { border-bottom: 1px solid #dee2e6; background: var(--body-background); } + +.section-card-header { + cursor: pointer; + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + font-weight: 600; +} + +.crew-card-body { + padding: 0.75rem; +} + +.boundary-items-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 0.5rem; +} + +.boundary-item { + padding: 0.5rem 0.65rem; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 4px; +} + +.boundary-item-header { + margin-bottom: 0.25rem; + font-weight: 600; + font-size: 0.85rem; +} + +.assignment-list { + display: flex; + flex-wrap: wrap; + gap: 0.25rem; + margin-bottom: 0.25rem; +} + +.crew-badge { + display: inline-flex; + align-items: center; + padding: 0.15rem 0.4rem; + border-radius: 4px; + background: rgba(255, 255, 255, 0.05); + font-size: 0.8rem; +} + +.add-crew-container { + display: flex; + gap: 0.4rem; + align-items: center; +} + +.add-crew-select { + flex: 1; +} diff --git a/client/src/vue_components/show/config/stage/StageTimeline.vue b/client/src/vue_components/show/config/stage/StageTimeline.vue index 14a3d217..c7436651 100644 --- a/client/src/vue_components/show/config/stage/StageTimeline.vue +++ b/client/src/vue_components/show/config/stage/StageTimeline.vue @@ -3,138 +3,150 @@
-
-
- - - Combined - - - Props - +
+
+
+ + + Combined + + + Props + + + Scenery + + - Scenery + - - - - -
-
-
- No allocation data to display for this view
- - - - - - {{ actGroup.actName }} - - - - - - {{ scene.name }} - - - - - - - - +
+
+ No allocation data to display for this view +
+ + + - {{ bar.tooltip }} - {{ bar.label }} + {{ actGroup.actName }} - - - - + - {{ row.name }} + {{ scene.name }} - - + + + + + + + + {{ bar.tooltip }} + + {{ bar.label }} + + + + + + + + + {{ row.name }} + + + + +
+
@@ -142,9 +154,13 @@ diff --git a/client/src/vue_components/show/config/stage/TimelineSidePanel.vue b/client/src/vue_components/show/config/stage/TimelineSidePanel.vue new file mode 100644 index 00000000..28994122 --- /dev/null +++ b/client/src/vue_components/show/config/stage/TimelineSidePanel.vue @@ -0,0 +1,538 @@ + + + + + diff --git a/client/src/vue_components/show/live/StageManagerPane.vue b/client/src/vue_components/show/live/StageManagerPane.vue index a91467c8..8e633037 100644 --- a/client/src/vue_components/show/live/StageManagerPane.vue +++ b/client/src/vue_components/show/live/StageManagerPane.vue @@ -121,6 +121,12 @@ :key="`set-scenery-${item.id}`" > {{ getSceneryDisplayName(item) }} +
+ {{ getCrewNamesForSettingItem(item, 'scenery', smPlanScene).join(', ') }} +

None

@@ -129,6 +135,12 @@
  • {{ getPropDisplayName(item) }} +
    + {{ getCrewNamesForSettingItem(item, 'prop', smPlanScene).join(', ') }} +

None

@@ -166,6 +178,14 @@ :key="`strike-scenery-${item.id}`" > {{ getSceneryDisplayName(item) }} +
+ {{ getCrewNamesForStrikingItem(item, 'scenery', smPlanScene).join(', ') }} +

None

@@ -177,6 +197,12 @@ :key="`strike-prop-${item.id}`" > {{ getPropDisplayName(item) }} +
+ {{ getCrewNamesForStrikingItem(item, 'prop', smPlanScene).join(', ') }} +

None

@@ -273,6 +299,9 @@ export default { 'SCENERY_LIST', 'PROP_TYPES_DICT', 'SCENERY_TYPES_DICT', + 'CREW_ASSIGNMENTS_BY_PROP', + 'CREW_ASSIGNMENTS_BY_SCENERY', + 'CREW_MEMBER_BY_ID', ]), }, watch: { @@ -331,6 +360,8 @@ export default { this.GET_SCENERY_ALLOCATIONS(), this.GET_PROP_TYPES(), this.GET_SCENERY_TYPES(), + this.GET_CREW_LIST(), + this.GET_CREW_ASSIGNMENTS(), ]); this.loaded = true; @@ -481,6 +512,30 @@ export default { (item) => !currentPropsIds.has(item.id) ); }, + formatCrewName(crew) { + if (!crew) return 'Unknown'; + return crew.last_name ? `${crew.first_name} ${crew.last_name}` : crew.first_name; + }, + getCrewNamesForSettingItem(item, itemType, scene) { + const assignments = + itemType === 'scenery' + ? this.CREW_ASSIGNMENTS_BY_SCENERY[item.id] || [] + : this.CREW_ASSIGNMENTS_BY_PROP[item.id] || []; + return assignments + .filter((a) => a.assignment_type === 'set' && a.scene_id === scene.id) + .map((a) => this.formatCrewName(this.CREW_MEMBER_BY_ID(a.crew_id))); + }, + getCrewNamesForStrikingItem(item, itemType, scene) { + const previousScene = this.getPreviousScene(scene); + if (!previousScene) return []; + const assignments = + itemType === 'scenery' + ? this.CREW_ASSIGNMENTS_BY_SCENERY[item.id] || [] + : this.CREW_ASSIGNMENTS_BY_PROP[item.id] || []; + return assignments + .filter((a) => a.assignment_type === 'strike' && a.scene_id === previousScene.id) + .map((a) => this.formatCrewName(this.CREW_MEMBER_BY_ID(a.crew_id))); + }, ...mapActions([ 'GET_ACT_LIST', 'GET_SCENE_LIST', @@ -490,6 +545,8 @@ export default { 'GET_SCENERY_ALLOCATIONS', 'GET_PROP_TYPES', 'GET_SCENERY_TYPES', + 'GET_CREW_LIST', + 'GET_CREW_ASSIGNMENTS', ]), }, }; @@ -646,4 +703,11 @@ export default { .plan-content-col.border-right { border-right: 1px solid #dee2e6; } + +.crew-names { + color: #6c757d; + font-size: 0.8rem; + padding-left: 0.5rem; + font-style: italic; +} diff --git a/dist/preload_modules.py b/dist/preload_modules.py index 1f46753e..9a6c95fd 100644 --- a/dist/preload_modules.py +++ b/dist/preload_modules.py @@ -24,10 +24,16 @@ print(f'Error preloading controllers.api: {e}') try: - importlib.import_module('controllers.api.auth') - print('Preloaded controllers.api.auth') + importlib.import_module('controllers.api.constants') + print('Preloaded controllers.api.constants') except Exception as e: - print(f'Error preloading controllers.api.auth: {e}') + print(f'Error preloading controllers.api.constants: {e}') + +try: + importlib.import_module('controllers.api.health') + print('Preloaded controllers.api.health') +except Exception as e: + print(f'Error preloading controllers.api.health: {e}') try: importlib.import_module('controllers.api.rbac') @@ -42,40 +48,40 @@ print(f'Error preloading controllers.api.settings: {e}') try: - importlib.import_module('controllers.api.websocket') - print('Preloaded controllers.api.websocket') + importlib.import_module('controllers.api.version') + print('Preloaded controllers.api.version') except Exception as e: - print(f'Error preloading controllers.api.websocket: {e}') + print(f'Error preloading controllers.api.version: {e}') try: - importlib.import_module('controllers.api.show') - print('Preloaded controllers.api.show') + importlib.import_module('controllers.api.websocket') + print('Preloaded controllers.api.websocket') except Exception as e: - print(f'Error preloading controllers.api.show: {e}') + print(f'Error preloading controllers.api.websocket: {e}') try: - importlib.import_module('controllers.api.show.cast') - print('Preloaded controllers.api.show.cast') + importlib.import_module('controllers.api.auth') + print('Preloaded controllers.api.auth') except Exception as e: - print(f'Error preloading controllers.api.show.cast: {e}') + print(f'Error preloading controllers.api.auth: {e}') try: - importlib.import_module('controllers.api.show.shows') - print('Preloaded controllers.api.show.shows') + importlib.import_module('controllers.api.auth.token') + print('Preloaded controllers.api.auth.token') except Exception as e: - print(f'Error preloading controllers.api.show.shows: {e}') + print(f'Error preloading controllers.api.auth.token: {e}') try: - importlib.import_module('controllers.api.show.sessions') - print('Preloaded controllers.api.show.sessions') + importlib.import_module('controllers.api.auth.user') + print('Preloaded controllers.api.auth.user') except Exception as e: - print(f'Error preloading controllers.api.show.sessions: {e}') + print(f'Error preloading controllers.api.auth.user: {e}') try: - importlib.import_module('controllers.api.show.microphones') - print('Preloaded controllers.api.show.microphones') + importlib.import_module('controllers.api.show') + print('Preloaded controllers.api.show') except Exception as e: - print(f'Error preloading controllers.api.show.microphones: {e}') + print(f'Error preloading controllers.api.show: {e}') try: importlib.import_module('controllers.api.show.acts') @@ -84,10 +90,10 @@ print(f'Error preloading controllers.api.show.acts: {e}') try: - importlib.import_module('controllers.api.show.scenes') - print('Preloaded controllers.api.show.scenes') + importlib.import_module('controllers.api.show.cast') + print('Preloaded controllers.api.show.cast') except Exception as e: - print(f'Error preloading controllers.api.show.scenes: {e}') + print(f'Error preloading controllers.api.show.cast: {e}') try: importlib.import_module('controllers.api.show.characters') @@ -101,6 +107,24 @@ except Exception as e: print(f'Error preloading controllers.api.show.cues: {e}') +try: + importlib.import_module('controllers.api.show.microphones') + print('Preloaded controllers.api.show.microphones') +except Exception as e: + print(f'Error preloading controllers.api.show.microphones: {e}') + +try: + importlib.import_module('controllers.api.show.scenes') + print('Preloaded controllers.api.show.scenes') +except Exception as e: + print(f'Error preloading controllers.api.show.scenes: {e}') + +try: + importlib.import_module('controllers.api.show.shows') + print('Preloaded controllers.api.show.shows') +except Exception as e: + print(f'Error preloading controllers.api.show.shows: {e}') + try: importlib.import_module('controllers.api.show.script') print('Preloaded controllers.api.show.script') @@ -108,16 +132,16 @@ print(f'Error preloading controllers.api.show.script: {e}') try: - importlib.import_module('controllers.api.show.script.config') - print('Preloaded controllers.api.show.script.config') + importlib.import_module('controllers.api.show.script.compiled') + print('Preloaded controllers.api.show.script.compiled') except Exception as e: - print(f'Error preloading controllers.api.show.script.config: {e}') + print(f'Error preloading controllers.api.show.script.compiled: {e}') try: - importlib.import_module('controllers.api.show.script.stage_direction_styles') - print('Preloaded controllers.api.show.script.stage_direction_styles') + importlib.import_module('controllers.api.show.script.config') + print('Preloaded controllers.api.show.script.config') except Exception as e: - print(f'Error preloading controllers.api.show.script.stage_direction_styles: {e}') + print(f'Error preloading controllers.api.show.script.config: {e}') try: importlib.import_module('controllers.api.show.script.revisions') @@ -131,12 +155,84 @@ except Exception as e: print(f'Error preloading controllers.api.show.script.script: {e}') +try: + importlib.import_module('controllers.api.show.script.stage_direction_styles') + print('Preloaded controllers.api.show.script.stage_direction_styles') +except Exception as e: + print(f'Error preloading controllers.api.show.script.stage_direction_styles: {e}') + +try: + importlib.import_module('controllers.api.show.session') + print('Preloaded controllers.api.show.session') +except Exception as e: + print(f'Error preloading controllers.api.show.session: {e}') + +try: + importlib.import_module('controllers.api.show.session.assign_tags') + print('Preloaded controllers.api.show.session.assign_tags') +except Exception as e: + print(f'Error preloading controllers.api.show.session.assign_tags: {e}') + +try: + importlib.import_module('controllers.api.show.session.sessions') + print('Preloaded controllers.api.show.session.sessions') +except Exception as e: + print(f'Error preloading controllers.api.show.session.sessions: {e}') + +try: + importlib.import_module('controllers.api.show.session.tags') + print('Preloaded controllers.api.show.session.tags') +except Exception as e: + print(f'Error preloading controllers.api.show.session.tags: {e}') + +try: + importlib.import_module('controllers.api.show.stage') + print('Preloaded controllers.api.show.stage') +except Exception as e: + print(f'Error preloading controllers.api.show.stage: {e}') + +try: + importlib.import_module('controllers.api.show.stage.crew') + print('Preloaded controllers.api.show.stage.crew') +except Exception as e: + print(f'Error preloading controllers.api.show.stage.crew: {e}') + +try: + importlib.import_module('controllers.api.show.stage.crew_assignments') + print('Preloaded controllers.api.show.stage.crew_assignments') +except Exception as e: + print(f'Error preloading controllers.api.show.stage.crew_assignments: {e}') + +try: + importlib.import_module('controllers.api.show.stage.helpers') + print('Preloaded controllers.api.show.stage.helpers') +except Exception as e: + print(f'Error preloading controllers.api.show.stage.helpers: {e}') + +try: + importlib.import_module('controllers.api.show.stage.props') + print('Preloaded controllers.api.show.stage.props') +except Exception as e: + print(f'Error preloading controllers.api.show.stage.props: {e}') + +try: + importlib.import_module('controllers.api.show.stage.scenery') + print('Preloaded controllers.api.show.stage.scenery') +except Exception as e: + print(f'Error preloading controllers.api.show.stage.scenery: {e}') + try: importlib.import_module('controllers.api.user') print('Preloaded controllers.api.user') except Exception as e: print(f'Error preloading controllers.api.user: {e}') +try: + importlib.import_module('controllers.api.user.overrides') + print('Preloaded controllers.api.user.overrides') +except Exception as e: + print(f'Error preloading controllers.api.user.overrides: {e}') + try: importlib.import_module('controllers.api.user.settings') print('Preloaded controllers.api.user.settings') @@ -145,16 +241,16 @@ # Preload all model modules try: - importlib.import_module('models.user') - print('Preloaded models.user') + importlib.import_module('models.cue') + print('Preloaded models.cue') except Exception as e: - print(f'Error preloading models.user: {e}') + print(f'Error preloading models.cue: {e}') try: - importlib.import_module('models.show') - print('Preloaded models.show') + importlib.import_module('models.mics') + print('Preloaded models.mics') except Exception as e: - print(f'Error preloading models.show: {e}') + print(f'Error preloading models.mics: {e}') try: importlib.import_module('models.models') @@ -163,16 +259,16 @@ print(f'Error preloading models.models: {e}') try: - importlib.import_module('models.session') - print('Preloaded models.session') + importlib.import_module('models.script') + print('Preloaded models.script') except Exception as e: - print(f'Error preloading models.session: {e}') + print(f'Error preloading models.script: {e}') try: - importlib.import_module('models.mics') - print('Preloaded models.mics') + importlib.import_module('models.session') + print('Preloaded models.session') except Exception as e: - print(f'Error preloading models.mics: {e}') + print(f'Error preloading models.session: {e}') try: importlib.import_module('models.settings') @@ -181,15 +277,21 @@ print(f'Error preloading models.settings: {e}') try: - importlib.import_module('models.script') - print('Preloaded models.script') + importlib.import_module('models.show') + print('Preloaded models.show') except Exception as e: - print(f'Error preloading models.script: {e}') + print(f'Error preloading models.show: {e}') try: - importlib.import_module('models.cue') - print('Preloaded models.cue') + importlib.import_module('models.stage') + print('Preloaded models.stage') except Exception as e: - print(f'Error preloading models.cue: {e}') + print(f'Error preloading models.stage: {e}') + +try: + importlib.import_module('models.user') + print('Preloaded models.user') +except Exception as e: + print(f'Error preloading models.user: {e}') print('Module preloading complete.') diff --git a/docs/images/config_show/stage_allocation_warning.png b/docs/images/config_show/stage_allocation_warning.png new file mode 100644 index 0000000000000000000000000000000000000000..b7fcc44968c9305000bdba822bb86faf9247f71b GIT binary patch literal 70033 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/images/config_show/stage_crew_timeline.png b/docs/images/config_show/stage_crew_timeline.png new file mode 100644 index 0000000000000000000000000000000000000000..beab91b090d20ec10063bf1d495a915d469f3c78 GIT binary patch literal 61738 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
RRJO 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
  • p>3}-u({m>x%%B3ReBIy@UpJjE@iR%aIgFPL;9G` zGxC%dPRkRJ<44v-&6;{HnGoxD#0vW@w^lE(GyBI0E{-HUes$Mj6Yc4f3zLin)xRuX z4wC@e1_15(!~JBgp|~+nahp!mv_A;`jN1HP?u$z-Jz~|)sn=s4vzlgr6zM(A&QFlj z3c-6*Z?F~jOfT+IqZ56AgV!>SU85AleoRd2fgc&(bI(%1EYOx?z#a%Um7X)ND~TB= zN|VBReGb~h54}!q0Wis_`I3FTDi|$wU7N_o?2%nB(hn2WJ`V&S^t}HOK;$^#pEbv> zjNWZq2yNSty_kACc7D%iDt8P1a`J^aki-6mUW)J!I6cA~(A5N-RUt~Rvv;egNVWd~ zK0coci?0hl3txGB7xd7;`jp}efH+iH19J}amT6MG+r$H(1wf)jxKmZ_9JnkOcy?D2 zI!vw`4l1Uix+>)1KT;)6Sg=7=pI!}FaB`W{omP~EGl;b^tsaNC1{(WR^2YPQ-$1@uNGIz&I0bJTs(i} K2lV%W_x=L{-EkKH literal 0 HcmV?d00001 diff --git a/docs/images/config_show/stage_manager_crew_assignments.png b/docs/images/config_show/stage_manager_crew_assignments.png new file mode 100644 index 0000000000000000000000000000000000000000..700a45007faf71fe0ac17d222c6f4cbe80d1c931 GIT binary patch literal 65565 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 literal 0 HcmV?d00001 diff --git a/docs/images/config_show/stage_manager_with_allocations.png b/docs/images/config_show/stage_manager_with_allocations.png index 39d67c11a5892ff42bae20aad39904898c3dfd1f..fa3a7dccf0e77b5c6bb894036d081ec9b29639b4 100644 GIT binary patch literal 68905 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 FzW{fIq+0+0 literal 135317 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�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 L63@7C_tF0cP$7h| diff --git a/docs/images/config_show/stage_timeline.png b/docs/images/config_show/stage_timeline.png index 53fa0617b3aeac4c30589312dfd9563daba2bee5..347ecfe26b692f93ba3af5af0b22997761e032a0 100644 GIT binary patch literal 67030 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 z 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 literal 124456 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 + + + + Unknown setting type {{ setting.type }} for setting {{ key }}. + Reset @@ -131,6 +144,16 @@ export default { } return mapping[fieldType]; }, + getChoiceOptions(setting) { + const options = []; + if (setting._nullable) { + options.push({ value: null, text: 'N/A' }); + } + setting.choice_options.forEach((option) => { + options.push({ value: option, text: option }); + }); + return options; + }, validateState(name) { const { $dirty, $error } = this.$v.editSettings[name]; return $dirty ? !$error : null; diff --git a/server/digi_server/app_server.py b/server/digi_server/app_server.py index 8359ac09..9e283908 100644 --- a/server/digi_server/app_server.py +++ b/server/digi_server/app_server.py @@ -17,7 +17,12 @@ from controllers import controllers from controllers.ws_controller import WebSocketController -from digi_server.logger import configure_db_logging, configure_file_logging, get_logger +from digi_server.logger import ( + configure_db_logging, + configure_file_logging, + configure_log_level, + get_logger, +) from digi_server.settings import Settings from models import models from models.cue import CueType @@ -362,23 +367,29 @@ async def _configure_logging(self): log_path = await self.digi_settings.get("log_path") file_size = await self.digi_settings.get("max_log_mb") backups = await self.digi_settings.get("log_backups") + log_level = await self.digi_settings.get("log_level") if log_path: self.app_log_handler = configure_file_logging( - log_path, file_size, backups, self.app_log_handler + log_path=log_path, + max_size_mb=file_size, + log_backups=backups, + handler=self.app_log_handler, ) + configure_log_level(log_level) # Database logging use_db_logging = await self.digi_settings.get("db_log_enabled") - if use_db_logging: - db_log_path = await self.digi_settings.get("db_log_path") - db_file_size = await self.digi_settings.get("db_max_log_mb") - db_backups = await self.digi_settings.get("db_log_backups") - self.db_file_handler = configure_db_logging( - log_path=db_log_path, - max_size_mb=db_file_size, - log_backups=db_backups, - handler=self.db_file_handler, - ) + db_log_path = await self.digi_settings.get("db_log_path") + db_file_size = await self.digi_settings.get("db_max_log_mb") + db_backups = await self.digi_settings.get("db_log_backups") + self.db_file_handler = configure_db_logging( + log_path=db_log_path, + max_size_mb=db_file_size, + log_backups=db_backups, + handler=self.db_file_handler, + log_level=log_level, + enable_db_logging=use_db_logging, + ) def _configure_rbac(self): self._db.register_delete_hook(self.rbac.rbac_db.check_object_deletion) diff --git a/server/digi_server/logger.py b/server/digi_server/logger.py index b9aed4bc..d63a69e6 100644 --- a/server/digi_server/logger.py +++ b/server/digi_server/logger.py @@ -6,6 +6,12 @@ logger = logging.getLogger("DigiScript") +ALL_LOGGERS = [ + logging.getLogger("tornado.access"), + logging.getLogger("tornado.application"), + logging.getLogger("tornado.general"), + logger, +] def get_logger(name: Optional[str] = None): @@ -14,26 +20,40 @@ def get_logger(name: Optional[str] = None): return logger -def configure_file_logging(log_path, max_size_mb=100, log_backups=5, handler=None): +def configure_log_level(log_level=logging.DEBUG): + for _logger in ALL_LOGGERS: + logger.info(f"Setting log level to {log_level} for logger: {_logger.name}") + _logger.setLevel(log_level) + + +def configure_file_logging( + log_path, + max_size_mb=100, + log_backups=5, + handler=None, +): size_bytes = max_size_mb * 1024 * 1024 - app_logger = get_logger() if handler: - app_logger.removeHandler(handler) + for _logger in ALL_LOGGERS: + _logger.removeHandler(handler) file_handler = RotatingFileHandler( log_path, maxBytes=size_bytes, backupCount=log_backups ) file_handler.setFormatter(LogFormatter(color=False)) - app_logger.addHandler(file_handler) - logging.getLogger("tornado.access").addHandler(file_handler) - logging.getLogger("tornado.application").addHandler(file_handler) - logging.getLogger("tornado.general").addHandler(file_handler) + for _logger in ALL_LOGGERS: + _logger.addHandler(file_handler) return file_handler def configure_db_logging( - log_level=logging.DEBUG, log_path=None, max_size_mb=100, log_backups=5, handler=None + log_level=logging.DEBUG, + log_path=None, + max_size_mb=100, + log_backups=5, + handler=None, + enable_db_logging=False, ): size_bytes = max_size_mb * 1024 * 1024 db_logger = logging.getLogger("sqlalchemy.engine") @@ -41,6 +61,11 @@ def configure_db_logging( if handler: db_logger.removeHandler(handler) + if not enable_db_logging: + logger.info(f"Disabling logger: {db_logger.name}") + return None + + logger.info(f"Setting log level to {log_level} for logger: {db_logger.name}") db_logger.setLevel(log_level) file_handler = None if log_path: @@ -75,3 +100,9 @@ def log_to_root(message, *args, **kwargs): setattr(logging, level_name, level_num) setattr(logging.getLoggerClass(), method_name, log_for_level) setattr(logging, method_name, log_to_root) + + +def get_level_names_by_order(): + levels = logging.getLevelNamesMapping() + sorted_levels = sorted(levels.items(), key=lambda x: x[1]) + return [name for name, _ in sorted_levels] diff --git a/server/digi_server/settings.py b/server/digi_server/settings.py index aface3a7..dfe386d4 100644 --- a/server/digi_server/settings.py +++ b/server/digi_server/settings.py @@ -4,11 +4,11 @@ import os import tomllib from pathlib import Path -from typing import TYPE_CHECKING, Dict +from typing import TYPE_CHECKING, Dict, Optional from tornado.locks import Lock -from digi_server.logger import get_logger +from digi_server.logger import get_level_names_by_order, get_logger from utils.file_watcher import IOLoopFileWatcher @@ -35,6 +35,8 @@ def get_version() -> str: class SettingsObject: + ALLOWED_TYPES = [str, bool, int] + def __init__( self, key, @@ -46,13 +48,38 @@ def __init__( display_name: str = "", help_text: str = "", hide_from_ui: bool = False, + choice_options: Optional[list] = None, ): - if val_type not in [str, bool, int]: + if val_type not in self.ALLOWED_TYPES: raise RuntimeError( f"Invalid type {val_type} for {key}. Allowed options are: " - f"[str, int, bool]" + f"{[t.__name__ for t in self.ALLOWED_TYPES]}." + ) + + if default is None and not nullable: + raise RuntimeError( + f"Default value for {key} cannot be None if setting is not nullable." ) + if default is not None and not isinstance(default, val_type): + raise RuntimeError( + f"Default value {default} for {key} is not of type {val_type.__name__}." + ) + + if choice_options is not None: + if len(choice_options) == 0: + raise RuntimeError(f"Choice options for {key} cannot be an empty list.") + + if any(not isinstance(option, val_type) for option in choice_options): + raise RuntimeError( + f"All choice options for {key} must be of type {val_type.__name__}." + ) + + if default not in choice_options: + raise RuntimeError( + f"Default value for {key} must be one of the choice options." + ) + self.key = key self.val_type = val_type self.value = None @@ -64,6 +91,7 @@ def __init__( self.display_name = display_name self.help_text = help_text self.hide_from_ui = hide_from_ui + self.choice_options = choice_options def set_to_default(self): self.value = self.default @@ -81,6 +109,12 @@ def set_value(self, value, spawn_callbacks=True): f"type {self.val_type}" ) + if self.choice_options is not None and value not in self.choice_options: + raise ValueError( + f"Value for {self.key} must be one of the following options: " + f"{self.choice_options}" + ) + changed = False if value != self.value: changed = True @@ -106,6 +140,8 @@ def as_json(self): "display_name": self.display_name, "help_text": self.help_text, "hide_from_ui": self.hide_from_ui, + "choice_options": self.choice_options, + "_nullable": self._nullable, } @@ -161,6 +197,15 @@ def __init__(self, application: DigiScriptServer, settings_path=None): hide_from_ui=True, ) self.define("debug_mode", bool, False, True, display_name="Enable Debug Mode") + self.define( + "log_level", + str, + "DEBUG", + True, + self._application.regen_logging, + display_name="Log Level", + choice_options=get_level_names_by_order(), + ) self.define( "log_path", str, @@ -254,6 +299,7 @@ def define( display_name: str = "", help_text: str = "", hide_from_ui: bool = False, + choice_options: Optional[list] = None, ): self.settings[key] = SettingsObject( key, @@ -265,6 +311,7 @@ def define( display_name, help_text, hide_from_ui, + choice_options, ) def file_deleted(self): From 2563803ab83688cd4bbaa29fc4254f10a1acb53d Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sun, 15 Feb 2026 13:33:44 +0000 Subject: [PATCH 27/43] [Fix] Require first user to be admin and auth user create endpoint --- server/controllers/api/auth/user.py | 86 +++++---- server/test/controllers/api/test_auth.py | 232 ++++++++++++++++++++--- server/utils/web/base_controller.py | 6 + 3 files changed, 259 insertions(+), 65 deletions(-) diff --git a/server/controllers/api/auth/user.py b/server/controllers/api/auth/user.py index 4af56f50..8049ed9d 100644 --- a/server/controllers/api/auth/user.py +++ b/server/controllers/api/auth/user.py @@ -15,62 +15,71 @@ api_authenticated, no_live_session, require_admin, - requires_show, ) @ApiRoute("auth/create", ApiVersion.V1) class UserCreateController(BaseAPIController): async def post(self): - data = escape.json_decode(self.request.body) + with self.make_session() as session: + # If there are no users, allow creation without authentication, otherwise require admin. + has_any_users = session.scalars(select(User)).first() is not None + if has_any_users: + self.requires_admin() - username = data.get("username", "") - if not username: - self.set_status(400) - await self.finish({"message": "Username missing"}) - return + data = escape.json_decode(self.request.body) - password = data.get("password", "") - if not password: - self.set_status(400) - await self.finish({"message": "Password missing"}) - return + username = data.get("username", "") + if not username: + self.set_status(400) + await self.finish({"message": "Username missing"}) + return - # Validate password strength - is_valid, error_msg = PasswordService.validate_password_strength(password) - if not is_valid: - self.set_status(400) - await self.finish({"message": error_msg}) - return + password = data.get("password", "") + if not password: + self.set_status(400) + await self.finish({"message": "Password missing"}) + return - is_admin = data.get("is_admin", False) + is_admin = data.get("is_admin", False) + if not has_any_users and not is_admin: + self.set_status(400) + await self.finish({"message": "First user must be an admin"}) + return - with self.make_session() as session: - conflict_user = session.scalars( - select(User).where(User.username == username) - ).first() - if conflict_user: + # Validate password strength + is_valid, error_msg = PasswordService.validate_password_strength(password) + if not is_valid: self.set_status(400) - await self.finish({"message": "Username already taken"}) + await self.finish({"message": error_msg}) return - hashed_password = await PasswordService.hash_password(password) + async with NamedLockRegistry.acquire(f"UserLock::{username}"): + conflict_user = session.scalars( + select(User).where(User.username == username) + ).first() + if conflict_user: + self.set_status(400) + await self.finish({"message": "Username already taken"}) + return - session.add( - User( - username=username, - password=hashed_password, - is_admin=is_admin, + hashed_password = await PasswordService.hash_password(password) + + session.add( + User( + username=username, + password=hashed_password, + is_admin=is_admin, + ) ) - ) - session.commit() + session.commit() - if is_admin: - await self.application.digi_settings.set("has_admin_user", True) + if is_admin: + await self.application.digi_settings.set("has_admin_user", True) - self.set_status(200) - await self.application.ws_send_to_all("NOOP", "GET_USERS", {}) - await self.finish({"message": "Successfully created user"}) + self.set_status(200) + await self.application.ws_send_to_all("NOOP", "GET_USERS", {}) + await self.finish({"message": "Successfully created user"}) @ApiRoute("auth/delete", ApiVersion.V1) @@ -233,7 +242,6 @@ async def post(self): class UsersHandler(BaseAPIController): @api_authenticated @require_admin - @requires_show def get(self): user_schema = UserSchema() with self.make_session() as session: diff --git a/server/test/controllers/api/test_auth.py b/server/test/controllers/api/test_auth.py index 825cba51..af589061 100644 --- a/server/test/controllers/api/test_auth.py +++ b/server/test/controllers/api/test_auth.py @@ -1,7 +1,6 @@ from sqlalchemy import select from tornado import escape -from models.show import Show, ShowScriptType from models.user import User from test.conftest import DigiScriptTestCase @@ -51,6 +50,21 @@ def test_create_admin(self): self.assertTrue("message" in response_body) self.assertEqual("Successfully created user", response_body["message"]) + def test_create_first_user_must_be_admin(self): + """Test that the first user created in the system must be an admin.""" + response = self.fetch( + "/api/v1/auth/create", + method="POST", + body=escape.json_encode( + {"username": "firstuser", "password": "password", "is_admin": False} + ), + ) + response_body = escape.json_decode(response.body) + + self.assertEqual(400, response.code) + self.assertTrue("message" in response_body) + self.assertEqual("First user must be an admin", response_body["message"]) + def test_create_user_duplicate_username(self): """Test POST /api/v1/auth/create with duplicate username. @@ -60,11 +74,28 @@ def test_create_user_duplicate_username(self): When a user with the same username already exists, the query should return that user and the endpoint should return a 400 error. """ - # Create an existing user directly in the database - with self._app.get_db().sessionmaker() as session: - existing_user = User(username="duplicate_test", password="hashed_pw") - session.add(existing_user) - session.commit() + # Create initial admin user + self.fetch( + "/api/v1/auth/create", + method="POST", + body=escape.json_encode( + { + "username": "duplicate_test", + "password": "adminpass", + "is_admin": True, + } + ), + ) + + # Login as admin + response = self.fetch( + "/api/v1/auth/login", + method="POST", + body=escape.json_encode( + {"username": "duplicate_test", "password": "adminpass"} + ), + ) + admin_token = escape.json_decode(response.body)["access_token"] # Try to create a user with the same username response = self.fetch( @@ -77,6 +108,7 @@ def test_create_user_duplicate_username(self): "is_admin": False, } ), + headers={"Authorization": f"Bearer {admin_token}"}, ) response_body = escape.json_decode(response.body) @@ -407,16 +439,6 @@ def test_delete_user(self): ), ) - # Create a test show (required by @requires_show decorator) - with self._app.get_db().sessionmaker() as session: - show = Show(name="Test Show", script_mode=ShowScriptType.FULL) - session.add(show) - session.flush() - show_id = show.id - session.commit() - - self._app.digi_settings.settings["current_show"].set_value(show_id) - # Login as admin to get token response = self.fetch( "/api/v1/auth/login", @@ -433,6 +455,7 @@ def test_delete_user(self): body=escape.json_encode( {"username": "userToDelete", "password": "password", "is_admin": False} ), + headers={"Authorization": f"Bearer {admin_token}"}, ) # Get the user ID @@ -479,16 +502,6 @@ def test_get_users(self): ), ) - # Create a test show (required by @requires_show decorator) - with self._app.get_db().sessionmaker() as session: - show = Show(name="Test Show", script_mode=ShowScriptType.FULL) - session.add(show) - session.flush() - show_id = show.id - session.commit() - - self._app.digi_settings.settings["current_show"].set_value(show_id) - # Login as admin response = self.fetch( "/api/v1/auth/login", @@ -505,6 +518,7 @@ def test_get_users(self): body=escape.json_encode( {"username": "user1", "password": "password", "is_admin": False} ), + headers={"Authorization": f"Bearer {admin_token}"}, ) self.fetch( "/api/v1/auth/create", @@ -512,6 +526,7 @@ def test_get_users(self): body=escape.json_encode( {"username": "user2", "password": "password", "is_admin": False} ), + headers={"Authorization": f"Bearer {admin_token}"}, ) # Get all users @@ -535,6 +550,23 @@ def test_get_users(self): def test_change_password_success(self): """Test successful password change with valid old password""" + # Create admin user + self.fetch( + "/api/v1/auth/create", + method="POST", + body=escape.json_encode( + {"username": "admin", "password": "adminpass", "is_admin": True} + ), + ) + + # Login as admin + response = self.fetch( + "/api/v1/auth/login", + method="POST", + body=escape.json_encode({"username": "admin", "password": "adminpass"}), + ) + admin_token = escape.json_decode(response.body)["access_token"] + # Create and login a user self.fetch( "/api/v1/auth/create", @@ -542,6 +574,7 @@ def test_change_password_success(self): body=escape.json_encode( {"username": "testuser", "password": "oldpass123", "is_admin": False} ), + headers={"Authorization": f"Bearer {admin_token}"}, ) response = self.fetch( @@ -593,6 +626,23 @@ def test_change_password_success(self): def test_change_password_incorrect_old_password(self): """Test password change fails with incorrect old password""" + # Create admin user + self.fetch( + "/api/v1/auth/create", + method="POST", + body=escape.json_encode( + {"username": "admin", "password": "adminpass", "is_admin": True} + ), + ) + + # Login as admin + response = self.fetch( + "/api/v1/auth/login", + method="POST", + body=escape.json_encode({"username": "admin", "password": "adminpass"}), + ) + admin_token = escape.json_decode(response.body)["access_token"] + # Create and login a user self.fetch( "/api/v1/auth/create", @@ -600,6 +650,7 @@ def test_change_password_incorrect_old_password(self): body=escape.json_encode( {"username": "testuser", "password": "oldpass123", "is_admin": False} ), + headers={"Authorization": f"Bearer {admin_token}"}, ) response = self.fetch( @@ -625,6 +676,23 @@ def test_change_password_incorrect_old_password(self): def test_change_password_missing_new_password(self): """Test password change fails when new password is missing""" + # Create admin user + self.fetch( + "/api/v1/auth/create", + method="POST", + body=escape.json_encode( + {"username": "admin", "password": "adminpass", "is_admin": True} + ), + ) + + # Login as admin + response = self.fetch( + "/api/v1/auth/login", + method="POST", + body=escape.json_encode({"username": "admin", "password": "adminpass"}), + ) + admin_token = escape.json_decode(response.body)["access_token"] + # Create and login a user self.fetch( "/api/v1/auth/create", @@ -632,6 +700,7 @@ def test_change_password_missing_new_password(self): body=escape.json_encode( {"username": "testuser", "password": "oldpass123", "is_admin": False} ), + headers={"Authorization": f"Bearer {admin_token}"}, ) response = self.fetch( @@ -655,6 +724,23 @@ def test_change_password_missing_new_password(self): def test_change_password_weak_password(self): """Test password change fails with weak new password""" + # Create admin user + self.fetch( + "/api/v1/auth/create", + method="POST", + body=escape.json_encode( + {"username": "admin", "password": "adminpass", "is_admin": True} + ), + ) + + # Login as admin + response = self.fetch( + "/api/v1/auth/login", + method="POST", + body=escape.json_encode({"username": "admin", "password": "adminpass"}), + ) + admin_token = escape.json_decode(response.body)["access_token"] + # Create and login a user self.fetch( "/api/v1/auth/create", @@ -662,6 +748,7 @@ def test_change_password_weak_password(self): body=escape.json_encode( {"username": "testuser", "password": "oldpass123", "is_admin": False} ), + headers={"Authorization": f"Bearer {admin_token}"}, ) response = self.fetch( @@ -699,6 +786,23 @@ def test_change_password_requires_authentication(self): def test_change_password_with_requires_password_change_flag(self): """Test password change works without old password when requires_password_change=True""" + # Create admin user + self.fetch( + "/api/v1/auth/create", + method="POST", + body=escape.json_encode( + {"username": "admin", "password": "adminpass", "is_admin": True} + ), + ) + + # Login as admin + response = self.fetch( + "/api/v1/auth/login", + method="POST", + body=escape.json_encode({"username": "admin", "password": "adminpass"}), + ) + admin_token = escape.json_decode(response.body)["access_token"] + # Create user via API to ensure password is properly hashed self.fetch( "/api/v1/auth/create", @@ -710,6 +814,7 @@ def test_change_password_with_requires_password_change_flag(self): "is_admin": False, } ), + headers={"Authorization": f"Bearer {admin_token}"}, ) # Set requires_password_change flag @@ -748,6 +853,23 @@ def test_change_password_with_requires_password_change_flag(self): def test_password_enforcement_blocks_regular_endpoints(self): """Test that requires_password_change blocks access to regular endpoints""" + # Create admin user + self.fetch( + "/api/v1/auth/create", + method="POST", + body=escape.json_encode( + {"username": "admin", "password": "adminpass", "is_admin": True} + ), + ) + + # Login as admin + response = self.fetch( + "/api/v1/auth/login", + method="POST", + body=escape.json_encode({"username": "admin", "password": "adminpass"}), + ) + admin_token = escape.json_decode(response.body)["access_token"] + # Create user with requires_password_change=True self.fetch( "/api/v1/auth/create", @@ -759,6 +881,7 @@ def test_password_enforcement_blocks_regular_endpoints(self): "is_admin": False, } ), + headers={"Authorization": f"Bearer {admin_token}"}, ) # Log in to get JWT @@ -790,6 +913,23 @@ def test_password_enforcement_blocks_regular_endpoints(self): def test_password_enforcement_allows_change_password_endpoint(self): """Test that requires_password_change allows access to change-password""" + # Create admin user + self.fetch( + "/api/v1/auth/create", + method="POST", + body=escape.json_encode( + {"username": "admin", "password": "adminpass", "is_admin": True} + ), + ) + + # Login as admin + response = self.fetch( + "/api/v1/auth/login", + method="POST", + body=escape.json_encode({"username": "admin", "password": "adminpass"}), + ) + admin_token = escape.json_decode(response.body)["access_token"] + # Create user with requires_password_change=True self.fetch( "/api/v1/auth/create", @@ -801,6 +941,7 @@ def test_password_enforcement_allows_change_password_endpoint(self): "is_admin": False, } ), + headers={"Authorization": f"Bearer {admin_token}"}, ) # Log in to get JWT @@ -833,6 +974,23 @@ def test_password_enforcement_allows_change_password_endpoint(self): def test_password_enforcement_allows_logout_endpoint(self): """Test that requires_password_change allows access to logout""" + # Create admin user + self.fetch( + "/api/v1/auth/create", + method="POST", + body=escape.json_encode( + {"username": "admin", "password": "adminpass", "is_admin": True} + ), + ) + + # Login as admin + response = self.fetch( + "/api/v1/auth/login", + method="POST", + body=escape.json_encode({"username": "admin", "password": "adminpass"}), + ) + admin_token = escape.json_decode(response.body)["access_token"] + # Create user with requires_password_change=True self.fetch( "/api/v1/auth/create", @@ -844,6 +1002,7 @@ def test_password_enforcement_allows_logout_endpoint(self): "is_admin": False, } ), + headers={"Authorization": f"Bearer {admin_token}"}, ) # Log in to get JWT @@ -900,6 +1059,7 @@ def test_admin_reset_password_success(self): body=escape.json_encode( {"username": "regularuser", "password": "userpass", "is_admin": False} ), + headers={"Authorization": f"Bearer {admin_token}"}, ) # Get user ID @@ -907,6 +1067,8 @@ def test_admin_reset_password_success(self): user = session.scalars( select(User).where(User.username == "regularuser") ).first() + if not user: + self.fail("User 'regularuser' not found in database") user_id = user.id # Admin resets user password @@ -979,6 +1141,23 @@ def test_admin_reset_password_cannot_reset_own(self): def test_admin_reset_password_requires_admin(self): """Test password reset requires admin privileges""" + # Create admin user + self.fetch( + "/api/v1/auth/create", + method="POST", + body=escape.json_encode( + {"username": "admin", "password": "adminpass", "is_admin": True} + ), + ) + + # Login as admin + response = self.fetch( + "/api/v1/auth/login", + method="POST", + body=escape.json_encode({"username": "admin", "password": "adminpass"}), + ) + admin_token = escape.json_decode(response.body)["access_token"] + # Create regular user self.fetch( "/api/v1/auth/create", @@ -986,6 +1165,7 @@ def test_admin_reset_password_requires_admin(self): body=escape.json_encode( {"username": "regularuser", "password": "userpass", "is_admin": False} ), + headers={"Authorization": f"Bearer {admin_token}"}, ) # Login as regular user diff --git a/server/utils/web/base_controller.py b/server/utils/web/base_controller.py index 5aa3b422..22a04e2f 100644 --- a/server/utils/web/base_controller.py +++ b/server/utils/web/base_controller.py @@ -121,6 +121,12 @@ async def prepare( self.current_show = show_schema.dump(show) return + def requires_admin(self): + if not self.current_user: + raise HTTPError(401, log_message="Not logged in") + if not self.current_user["is_admin"]: + raise HTTPError(403, log_message="Admin access required") + def requires_role(self, resource: db.Model, role: Role): if not self.current_user: raise HTTPError(401, log_message="Not logged in") From a3a1d9012330f935bb1af3420bcfafd56f2036bd Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sun, 15 Feb 2026 13:42:05 +0000 Subject: [PATCH 28/43] Auto login initial admin user upon creation --- client/src/vue_components/user/CreateUser.vue | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/src/vue_components/user/CreateUser.vue b/client/src/vue_components/user/CreateUser.vue index 1783d9c4..fcdf2f19 100644 --- a/client/src/vue_components/user/CreateUser.vue +++ b/client/src/vue_components/user/CreateUser.vue @@ -115,10 +115,16 @@ export default { event.preventDefault(); } else { await this.CREATE_USER(this.state); + if (this.isFirstAdmin) { + await this.USER_LOGIN({ + username: this.state.username, + password: this.state.password, + }); + } this.$emit('created_user'); } }, - ...mapActions(['CREATE_USER']), + ...mapActions(['CREATE_USER', 'USER_LOGIN']), }, }; From 894f29f2040d7616185ef2031a80a9c0f6999047 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sun, 15 Feb 2026 14:19:33 +0000 Subject: [PATCH 29/43] [Fix] Remove disable behaviour on Settings->Users tab (#914) --- client/src/views/config/ConfigView.vue | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/client/src/views/config/ConfigView.vue b/client/src/views/config/ConfigView.vue index f3aa0d04..6c3a226c 100644 --- a/client/src/views/config/ConfigView.vue +++ b/client/src/views/config/ConfigView.vue @@ -10,7 +10,7 @@ - + @@ -20,8 +20,6 @@ From a7ddebeef9f73ffc90fbebe84bfb3312f2fad3fb Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sun, 15 Feb 2026 14:40:27 +0000 Subject: [PATCH 30/43] [Docs] Add link to DeepWiki site --- documentation/development.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/development.md b/documentation/development.md index ddcca263..b1b317d3 100644 --- a/documentation/development.md +++ b/documentation/development.md @@ -31,6 +31,8 @@ DigiScript consists of three main components: - **`client/`** - Vue.js 2 frontend (builds to `server/static/` for web, or `client/dist-electron/` for Electron) - **`electron/`** - Electron desktop application wrapper +Refer to [DeepWiki](https://deepwiki.com/dreamteamprod/DigiScript) for detailed documentation on the architecture and design. + ## Building the Web Client ```shell From 0c4609658085e9ad3e5dc7d2a3ebf04a1a8bb5c7 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sun, 15 Feb 2026 14:53:06 +0000 Subject: [PATCH 31/43] [Fix] Users table empty when now show loaded --- client/src/store/modules/user/user.js | 18 +++++++----------- .../src/vue_components/config/ConfigUsers.vue | 4 ++-- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/client/src/store/modules/user/user.js b/client/src/store/modules/user/user.js index 43629282..31185fa8 100644 --- a/client/src/store/modules/user/user.js +++ b/client/src/store/modules/user/user.js @@ -10,7 +10,7 @@ export default { state: { currentUser: null, currentRbac: null, - showUsers: [], + users: [], authToken: localStorage.getItem('digiscript_auth_token') || null, tokenRefreshInterval: null, }, @@ -18,8 +18,8 @@ export default { SET_CURRENT_USER(state, user) { state.currentUser = user; }, - SET_SHOW_USERS(state, users) { - state.showUsers = users; + SET_USERS(state, users) { + state.users = users; }, SET_CURRENT_RBAC(state, rbac) { state.currentRbac = rbac; @@ -41,17 +41,13 @@ export default { }, actions: { async GET_USERS(context) { - if ( - context.getters.CURRENT_USER == null || - !context.getters.CURRENT_USER.is_admin || - context.rootGetters.CURRENT_SHOW == null - ) { + if (context.getters.CURRENT_USER == null || !context.getters.CURRENT_USER.is_admin) { return; } const response = await fetch(makeURL('/api/v1/auth/users')); if (response.ok) { const users = await response.json(); - await context.commit('SET_SHOW_USERS', users.users); + await context.commit('SET_USERS', users.users); } else { log.error('Unable to get users'); Vue.$toast.error('Unable to fetch users!'); @@ -270,8 +266,8 @@ export default { CURRENT_USER(state) { return state.currentUser; }, - SHOW_USERS(state) { - return state.showUsers; + USERS(state) { + return state.users; }, CURRENT_USER_RBAC(state) { return state.currentRbac; diff --git a/client/src/vue_components/config/ConfigUsers.vue b/client/src/vue_components/config/ConfigUsers.vue index 6d8fbd4f..df878161 100644 --- a/client/src/vue_components/config/ConfigUsers.vue +++ b/client/src/vue_components/config/ConfigUsers.vue @@ -2,7 +2,7 @@ - + @@ -80,7 +80,7 @@ export default { }; }, computed: { - ...mapGetters(['SHOW_USERS', 'CURRENT_SHOW', 'CURRENT_USER']), + ...mapGetters(['USERS', 'CURRENT_SHOW', 'CURRENT_USER']), }, async mounted() { await this.getUsers(); From b408bacc997b92de627fda27d21be9df647514ba Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sun, 15 Feb 2026 15:42:47 +0000 Subject: [PATCH 32/43] Allow multiple admin users to be created --- .../src/vue_components/config/ConfigUsers.vue | 31 +++++++++++++++++-- client/src/vue_components/user/CreateUser.vue | 20 ++++++++++-- server/controllers/api/auth/user.py | 14 +++++++-- 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/client/src/vue_components/config/ConfigUsers.vue b/client/src/vue_components/config/ConfigUsers.vue index df878161..4fe926e5 100644 --- a/client/src/vue_components/config/ConfigUsers.vue +++ b/client/src/vue_components/config/ConfigUsers.vue @@ -4,7 +4,21 @@ + + + + From 3de1161f8e3c740eebd7892d8e76fa3415c84794 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Thu, 19 Feb 2026 17:49:14 +0000 Subject: [PATCH 34/43] Fix CI: update nodelint.yml actions and regenerate electron lock file (#925) - Upgrade checkout@v3 and setup-node@v3 to v4 in nodelint.yml - Regenerate electron/package-lock.json on Linux to add missing optional dependency entries (encoding@0.1.13, iconv-lite@0.6.3) required by minipass-fetch but omitted when generated on macOS Co-authored-by: Claude Sonnet 4.6 --- .github/workflows/nodelint.yml | 8 ++++---- electron/package-lock.json | 33 +++++++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/.github/workflows/nodelint.yml b/.github/workflows/nodelint.yml index 2cbaa93b..786f9fea 100644 --- a/.github/workflows/nodelint.yml +++ b/.github/workflows/nodelint.yml @@ -11,8 +11,8 @@ jobs: run: working-directory: ./client steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 24 - run: npm ci @@ -23,8 +23,8 @@ jobs: run: working-directory: ./electron steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 24 - run: npm ci diff --git a/electron/package-lock.json b/electron/package-lock.json index 8ad59fa3..2c53e314 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1169,7 +1169,6 @@ "integrity": "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^3.0.1", "@inquirer/confirm": "^4.0.1", @@ -1801,7 +1800,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1877,7 +1875,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -2119,7 +2116,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3478,6 +3474,31 @@ "dev": true, "license": "MIT" }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -3595,7 +3616,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3656,7 +3676,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -6207,7 +6226,6 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -7626,7 +7644,6 @@ "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", From bfbb47d94a274b4d1087478a1c6f5ba2554bd4a3 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sat, 21 Feb 2026 00:55:38 +0000 Subject: [PATCH 35/43] Client and Server logging improvements (#923) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Implement client-side logging with a new server endpoint, configurable settings, and dedicated tests. * Lint and format * Fix client_log_level missing regen_logging callback + add help_text to file settings - `client_log_level` was defined without a callback_fn, so changing the level at runtime via the settings UI would update the frontend (which reads SETTINGS dynamically on each log call) but leave the Python client_logger frozen at its startup level, silently dropping messages. Added `self._application.regen_logging` as the 5th positional arg to match the pattern used by `log_level` and the other client_log_* settings. - Added `help_text` to `client_log_path`, `client_max_log_mb`, and `client_log_backups` to match the style used by `client_log_enabled` and `client_log_level`. Co-Authored-By: Claude Sonnet 4.6 * Fix client log level name mismatch and add per-user console log level setting - Fix client_log_level setting to use loglevel npm names (TRACE/DEBUG/INFO/WARN/ERROR) instead of Python logging names, and update help text/display name for clarity - Add CLIENT_LEVEL_MAP and map_client_level() to translate loglevel npm names to Python logging integers, fixing WARN→WARNING and TRACE→5 mappings - Use map_client_level() in process_logs() replacing getattr() which silently fell back to INFO for unrecognised names like TRACE - Add console_log_level to UserSettings model (non-nullable, default WARN) with a CHECK constraint restricting values to valid loglevel names - Add Alembic migration for the new column with server_default and check constraint - Add store.watch() in initRemoteLogging() to reactively apply the user's console_log_level preference to loglevel without persisting to localStorage - Add console log level selector to user settings UI with tooltip help text - Add 9 unit tests for map_client_level() covering all named levels, case-insensitivity, and unknown level fallback Co-Authored-By: Claude Sonnet 4.6 * feat: Add in-browser log viewer with SSE live streaming Adds a "Logs" tab to the admin config page providing a terminal-style log console backed by an in-memory circular buffer (LogBufferHandler). Key components: - LogBufferHandler: deque-backed handler with pub/sub for live delivery - GET /api/v1/logs/view: paginated snapshot with level/search/username filters - GET /api/v1/logs/stream: SSE endpoint with backfill + real-time push - ConfigLogs.vue: dark console UI; uses fetch()+ReadableStream for SSE so http-interceptor.js handles auth automatically; sliding window keeps at most "Max lines" entries in memory during live streaming - 42 backend tests covering buffer behaviour, auth, filters, and SSE backfill Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- client/src/js/logger.js | 127 ++++++ client/src/main.js | 2 + client/src/views/config/ConfigView.vue | 8 +- .../src/vue_components/config/ConfigLogs.vue | 403 ++++++++++++++++++ .../vue_components/user/settings/Settings.vue | 28 ++ ..._add_console_log_level_to_user_settings.py | 47 ++ server/controllers/api/logging.py | 109 +++++ server/controllers/api/logs_viewer.py | 241 +++++++++++ server/digi_server/app_server.py | 24 ++ server/digi_server/log_buffer.py | 133 ++++++ server/digi_server/logger.py | 88 ++++ server/digi_server/settings.py | 55 +++ server/models/user.py | 10 +- server/test/controllers/api/test_logging.py | 72 ++++ .../test/controllers/api/test_logs_viewer.py | 396 +++++++++++++++++ server/test/digi_server/test_log_buffer.py | 238 +++++++++++ server/test/digi_server/test_logger.py | 40 ++ server/utils/web/base_controller.py | 9 +- server/utils/web/route.py | 10 +- 19 files changed, 2034 insertions(+), 6 deletions(-) create mode 100644 client/src/js/logger.js create mode 100644 client/src/vue_components/config/ConfigLogs.vue create mode 100644 server/alembic_config/versions/c1db8c1f4e37_add_console_log_level_to_user_settings.py create mode 100644 server/controllers/api/logging.py create mode 100644 server/controllers/api/logs_viewer.py create mode 100644 server/digi_server/log_buffer.py create mode 100644 server/test/controllers/api/test_logging.py create mode 100644 server/test/controllers/api/test_logs_viewer.py create mode 100644 server/test/digi_server/test_log_buffer.py create mode 100644 server/test/digi_server/test_logger.py diff --git a/client/src/js/logger.js b/client/src/js/logger.js new file mode 100644 index 00000000..5cfbf58f --- /dev/null +++ b/client/src/js/logger.js @@ -0,0 +1,127 @@ +import log from 'loglevel'; +import { makeURL } from '@/js/utils'; +import store from '@/store/store'; + +let isInitialized = false; + +const FLUSH_INTERVAL_MS = 1000; +const MAX_QUEUE_SIZE = 50; + +let logQueue = []; +let flushTimer = null; + +/** + * Buffer a log entry and schedule a debounced flush. + * Flushes immediately if the queue reaches MAX_QUEUE_SIZE. + */ +function enqueueLog(level, message, extra) { + logQueue.push({ level, message, extra }); + if (logQueue.length >= MAX_QUEUE_SIZE) { + flushQueue(); + return; + } + clearTimeout(flushTimer); + flushTimer = setTimeout(flushQueue, FLUSH_INTERVAL_MS); +} + +/** + * Send all queued log entries to the server as a single batch request. + * Uses fetch directly to avoid potential infinite loops with HTTP interceptors. + */ +function flushQueue() { + clearTimeout(flushTimer); + flushTimer = null; + if (logQueue.length === 0) return; + + const batch = logQueue.splice(0); + + const token = store.getters.AUTH_TOKEN; + const headers = { 'Content-Type': 'application/json' }; + if (token) headers['Authorization'] = `Bearer ${token}`; + + fetch(makeURL('/api/v1/logs/batch'), { + method: 'POST', + headers, + body: JSON.stringify({ batch: batch }), + }).catch(() => { + // Intentionally ignore errors to prevent log flooding or infinite loops + }); +} + +/** + * Initialize remote logging. + * This hooks into the loglevel library to send logs to the server. + */ +export function initRemoteLogging() { + if (isInitialized) return; + isInitialized = true; + + const originalFactory = log.methodFactory; + + const levels = { TRACE: 0, DEBUG: 1, INFO: 2, WARN: 3, ERROR: 4, SILENT: 5 }; + + log.methodFactory = function (methodName, logLevel, loggerName) { + const rawMethod = originalFactory(methodName, logLevel, loggerName); + + return function (message, ...args) { + // Always call the standard log method first + rawMethod(message, ...args); + + const settings = store.getters.SETTINGS; + if (!settings || !settings.client_log_enabled) return; + + const currentLevelName = methodName.toUpperCase(); + const currentLevel = levels[currentLevelName] ?? 2; + const minLevel = levels[(settings.client_log_level || 'INFO').toUpperCase()] ?? 2; + if (currentLevel < minLevel) return; + + let finalMessage = message; + if (typeof message !== 'string') { + try { + finalMessage = JSON.stringify(message); + } catch { + finalMessage = String(message); + } + } + + const extra = {}; + if (args.length > 0) { + extra.args = args.map((arg) => + arg instanceof Error ? { message: arg.message, stack: arg.stack, name: arg.name } : arg + ); + } + + enqueueLog(currentLevelName, finalMessage, extra); + }; + }; + + // Re-apply the level to trigger the factory update + log.setLevel(log.getLevel()); + + // Keep browser console level in sync with the user's per-account preference. + // The false arg prevents loglevel from persisting this to localStorage. + store.watch( + (state, getters) => getters.USER_SETTINGS?.console_log_level, + (newLevel) => { + if (newLevel) log.setLevel(newLevel.toLowerCase(), false); + }, + { immediate: true } + ); + + // Intercept global errors — enqueue directly instead of routing through + // log.error() so the factory is never re-entered from here. + window.addEventListener('error', (event) => { + enqueueLog('ERROR', `Unhandled Error: ${event.message}`, { + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + stack: event.error ? event.error.stack : null, + }); + }); + + window.addEventListener('unhandledrejection', (event) => { + enqueueLog('ERROR', `Unhandled Promise Rejection: ${event.reason}`, {}); + }); + + enqueueLog('INFO', 'Remote logging initialized', {}); +} diff --git a/client/src/main.js b/client/src/main.js index 9463ed85..c3e1d153 100644 --- a/client/src/main.js +++ b/client/src/main.js @@ -12,6 +12,7 @@ import App from './App.vue'; import router from './router'; import setupHttpInterceptor from './js/http-interceptor'; import { getWebSocketURL, isElectron } from '@/js/platform'; +import { initRemoteLogging } from '@/js/logger'; import './assets/styles/dark.scss'; import 'vue-toast-notification/dist/theme-sugar.css'; @@ -19,6 +20,7 @@ import 'vue-multiselect/dist/vue-multiselect.min.css'; import 'splitpanes/dist/splitpanes.css'; setupHttpInterceptor(); +initRemoteLogging(); Vue.use(BootstrapVue); Vue.use(IconsPlugin); diff --git a/client/src/views/config/ConfigView.vue b/client/src/views/config/ConfigView.vue index 351d7461..f3a55154 100644 --- a/client/src/views/config/ConfigView.vue +++ b/client/src/views/config/ConfigView.vue @@ -3,7 +3,7 @@

    DigiScript Config

    - + @@ -16,6 +16,9 @@ + + +
    @@ -27,9 +30,10 @@ import ConfigSystem from '@/vue_components/config/ConfigSystem.vue'; import ConfigSettings from '@/vue_components/config/ConfigSettings.vue'; import ConfigUsers from '@/vue_components/config/ConfigUsers.vue'; import ConfigShows from '@/vue_components/config/ConfigShows.vue'; +import ConfigLogs from '@/vue_components/config/ConfigLogs.vue'; export default { name: 'ConfigView', - components: { ConfigShows, ConfigUsers, ConfigSettings, ConfigSystem }, + components: { ConfigShows, ConfigUsers, ConfigSettings, ConfigSystem, ConfigLogs }, }; diff --git a/client/src/vue_components/config/ConfigLogs.vue b/client/src/vue_components/config/ConfigLogs.vue new file mode 100644 index 00000000..e91cda29 --- /dev/null +++ b/client/src/vue_components/config/ConfigLogs.vue @@ -0,0 +1,403 @@ + + + diff --git a/client/src/vue_components/user/settings/Settings.vue b/client/src/vue_components/user/settings/Settings.vue index e9253e31..747de65e 100644 --- a/client/src/vue_components/user/settings/Settings.vue +++ b/client/src/vue_components/user/settings/Settings.vue @@ -60,6 +60,24 @@ :state="validateState('script_text_alignment')" /> + + + + Reset @@ -95,12 +113,21 @@ export default { script_auto_save_interval: 10, cue_position_right: false, script_text_alignment: TEXT_ALIGNMENT.CENTER, + console_log_level: 'WARN', }, textAlignmentOptions: [ { value: TEXT_ALIGNMENT.LEFT, text: 'Left' }, { value: TEXT_ALIGNMENT.CENTER, text: 'Center' }, { value: TEXT_ALIGNMENT.RIGHT, text: 'Right' }, ], + consoleLogLevelOptions: [ + { value: 'TRACE', text: 'TRACE' }, + { value: 'DEBUG', text: 'DEBUG' }, + { value: 'INFO', text: 'INFO' }, + { value: 'WARN', text: 'WARN' }, + { value: 'ERROR', text: 'ERROR' }, + { value: 'SILENT', text: 'SILENT' }, + ], toggle: 0, }; }, @@ -127,6 +154,7 @@ export default { required, integer, }, + console_log_level: { required }, }, }, mounted() { diff --git a/server/alembic_config/versions/c1db8c1f4e37_add_console_log_level_to_user_settings.py b/server/alembic_config/versions/c1db8c1f4e37_add_console_log_level_to_user_settings.py new file mode 100644 index 00000000..b79a73cc --- /dev/null +++ b/server/alembic_config/versions/c1db8c1f4e37_add_console_log_level_to_user_settings.py @@ -0,0 +1,47 @@ +"""Add console_log_level to user_settings + +Revision ID: c1db8c1f4e37 +Revises: fbb1b6bd8707 +Create Date: 2026-02-20 22:51:22.081714 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision: str = "c1db8c1f4e37" +down_revision: Union[str, None] = "fbb1b6bd8707" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("user_settings", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "console_log_level", + sa.String(), + nullable=False, + server_default="WARN", + ) + ) + batch_op.create_check_constraint( + "ck_user_settings_console_log_level", + "console_log_level IN ('TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'SILENT')", + ) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("user_settings", schema=None) as batch_op: + batch_op.drop_constraint("ck_user_settings_console_log_level", type_="check") + batch_op.drop_column("console_log_level") + + # ### end Alembic commands ### diff --git a/server/controllers/api/logging.py b/server/controllers/api/logging.py new file mode 100644 index 00000000..75856952 --- /dev/null +++ b/server/controllers/api/logging.py @@ -0,0 +1,109 @@ +from typing import Any, Dict, List + +from tornado import escape, httputil + +from digi_server.app_server import DigiScriptServer +from digi_server.logger import get_logger, map_client_level +from utils.web.base_controller import BaseAPIController +from utils.web.route import ApiRoute, ApiVersion + + +class ClientLoggingBase(BaseAPIController): + def __init__( + self, + application: DigiScriptServer, + request: httputil.HTTPServerRequest, + **kwargs: Any, + ): + super().__init__(application, request, **kwargs) + self.client_logger = get_logger("Client") + + def process_logs(self, entries: List[Dict]): + # Build request-level context added to every entry's extra + request_extra = { + "remote_ip": self.request.remote_ip, + "agent": self.request.headers.get("User-Agent", "Unknown"), + } + if self.current_user: + request_extra["user_id"] = self.current_user.get("id") + request_extra["username"] = self.current_user.get("username") + + for entry in entries: + level = entry.get("level", "INFO").upper() + message = entry.get("message", "") + extra = {**entry.get("extra", {}), **request_extra} + + log_level = map_client_level(level) + + log_msg = f"[Client] {message}" + if extra: + log_msg += f" | Extra: {extra}" + + # Pass structured fields via extra so LogBufferHandler can read them. + # Tornado's LogFormatter ignores unknown extra fields, so file output + # is unaffected. + self.client_logger.log( + log_level, + log_msg, + extra={ + "user_id": request_extra.get("user_id"), + "username": request_extra.get("username"), + "remote_ip": request_extra.get("remote_ip"), + }, + ) + + self.set_status(200) + self.write({"status": "OK"}) + + +@ApiRoute("logs/batch", ApiVersion.V1, ignore_logging=True) +class ClientLoggingBatchController(ClientLoggingBase): + async def post(self): + client_log_enabled = await self.application.digi_settings.get( + "client_log_enabled" + ) + if not client_log_enabled: + self.set_status(403) + self.write({"message": "Client logging is disabled"}) + return + + try: + data = escape.json_decode(self.request.body) + except ValueError: + self.set_status(400) + self.write({"message": "Invalid JSON"}) + return + + if not isinstance(data, dict): + self.set_status(400) + self.write({"message": "Expected JSON object with 'batch' key"}) + return + + batch = data.get("batch") + if not isinstance(batch, list): + self.set_status(400) + self.write({"message": "Expected 'batch' to be a list of log entries"}) + return + + self.process_logs(batch) + + +@ApiRoute("logs", ApiVersion.V1, ignore_logging=True) +class ClientLoggingController(ClientLoggingBase): + async def post(self): + client_log_enabled = await self.application.digi_settings.get( + "client_log_enabled" + ) + if not client_log_enabled: + self.set_status(403) + self.write({"message": "Client logging is disabled"}) + return + + try: + data = escape.json_decode(self.request.body) + except ValueError: + self.set_status(400) + self.write({"message": "Invalid JSON"}) + return + + self.process_logs([data]) diff --git a/server/controllers/api/logs_viewer.py b/server/controllers/api/logs_viewer.py new file mode 100644 index 00000000..d934548b --- /dev/null +++ b/server/controllers/api/logs_viewer.py @@ -0,0 +1,241 @@ +"""Log viewer API endpoints. + +Provides two admin-only endpoints backed by the in-memory log buffers +populated by :class:`~digi_server.log_buffer.LogBufferHandler`: + +* ``GET /api/v1/logs/view`` — paginated one-shot snapshot +* ``GET /api/v1/logs/stream`` — Server-Sent Events (SSE) live stream with + backfill of existing entries +""" + +import asyncio +import json +import logging + +from digi_server.log_buffer import get_client_buffer, get_server_buffer +from utils.web.base_controller import BaseAPIController +from utils.web.route import ApiRoute, ApiVersion +from utils.web.web_decorators import require_admin + + +# Map level name → minimum level_no for "greater-than-or-equal" filtering. +# WARN is accepted as an alias for WARNING (mirrors loglevel npm behaviour). +_LEVEL_ALIASES = { + "WARN": logging.WARNING, + "WARNING": logging.WARNING, + "DEBUG": logging.DEBUG, + "INFO": logging.INFO, + "ERROR": logging.ERROR, + "CRITICAL": logging.CRITICAL, + "TRACE": 5, # Custom level registered in main.py +} + +_MAX_LIMIT = 1000 + +# Sentinel placed on the SSE queue when the client disconnects, so the +# awaiting coroutine can exit cleanly without waiting for the next timeout. +_STREAM_CLOSED = object() + + +def _parse_source(raw: str) -> str: + """Normalise the ``source`` query parameter. + + :param raw: Raw value from the query string. + :returns: ``"client"`` or ``"server"``. + """ + return "client" if raw.lower() == "client" else "server" + + +def _filter_entries( + entries: list, + level_name: str, + search: str, + username_filter: str, + source: str, +) -> list: + """Apply all active filters to *entries* and return the matching subset. + + :param entries: List of entry dicts to filter. + :param level_name: Uppercase level name (e.g. ``"ERROR"``); empty means + no level filter. + :param search: Lowercase search string; empty means no search filter. + :param username_filter: Lowercase username substring; only applied when + *source* is ``"client"``; empty means no filter. + :param source: ``"server"`` or ``"client"``. + :returns: Filtered list of entry dicts. + """ + if level_name: + min_level_no = _LEVEL_ALIASES.get(level_name) + if min_level_no is not None: + entries = [e for e in entries if e["level_no"] >= min_level_no] + + if search: + entries = [e for e in entries if search in e["message"].lower()] + + if username_filter and source == "client": + entries = [ + e + for e in entries + if e.get("username") and username_filter in e["username"].lower() + ] + + return entries + + +@ApiRoute("logs/view", ApiVersion.V1, ignore_logging=True) +class LogViewerController(BaseAPIController): + """Return a filtered snapshot of the in-memory log buffer. + + Query parameters + ---------------- + source : str + ``"server"`` (default) or ``"client"``. + level : str + Minimum level name. Empty string (default) means all levels. + Accepts ``TRACE``, ``DEBUG``, ``INFO``, ``WARN``/``WARNING``, + ``ERROR``, ``CRITICAL``. + search : str + Case-insensitive substring match on the ``message`` field. + username : str + (Client source only) case-insensitive substring match on the + ``username`` field. + limit : int + Maximum number of entries to return (capped at 1000, default 500). + offset : int + Number of entries to skip before returning (default 0). + """ + + @require_admin + async def get(self): + """Handle ``GET /api/v1/logs/view``. + + :raises tornado.web.HTTPError: 401 if not authenticated, + 403 if not admin. + """ + source = _parse_source(self.get_argument("source", "server")) + level_name = self.get_argument("level", "").upper() + search = self.get_argument("search", "").lower() + username_filter = self.get_argument("username", "").lower() + + try: + limit = min(int(self.get_argument("limit", "500")), _MAX_LIMIT) + except ValueError: + limit = 500 + + try: + offset = max(int(self.get_argument("offset", "0")), 0) + except ValueError: + offset = 0 + + if source == "client": + entries = get_client_buffer().get_entries() + else: + entries = get_server_buffer().get_entries() + + entries = _filter_entries(entries, level_name, search, username_filter, source) + + total = len(entries) + page = entries[offset : offset + limit] + + self.write( + { + "entries": page, + "total": total, + "returned": len(page), + "source": source, + } + ) + + +@ApiRoute("logs/stream", ApiVersion.V1, ignore_logging=True) +class LogStreamController(BaseAPIController): + """Stream log entries to the client using Server-Sent Events (SSE). + + On connection the handler first sends all existing (backfill) entries from + the buffer that match the active filters, then emits new entries in + real-time as they arrive. A ``: keepalive`` comment is written every 20 s + to prevent proxies and browsers from closing an idle connection. + + Query parameters + ---------------- + Same as :class:`LogViewerController` except ``limit`` and ``offset`` which + are not applicable to a live stream. + + SSE event format + ---------------- + Each event is a single ``data:`` line containing a JSON-encoded entry dict, + followed by a blank line:: + + data: {"ts": "...", "level": "INFO", ...} + + """ + + def on_connection_close(self): + """Called by Tornado when the client closes the connection. + + Sets a flag and pushes the :data:`_STREAM_CLOSED` sentinel onto the + queue so the suspended ``get()`` coroutine wakes up and exits. + """ + self._sse_closed = True + if hasattr(self, "_sse_queue"): + self._sse_queue.put_nowait(_STREAM_CLOSED) + + @require_admin + async def get(self): + """Handle ``GET /api/v1/logs/stream``. + + :raises tornado.web.HTTPError: 401 if not authenticated, + 403 if not admin. + """ + source = _parse_source(self.get_argument("source", "server")) + level_name = self.get_argument("level", "").upper() + search = self.get_argument("search", "").lower() + username_filter = self.get_argument("username", "").lower() + + self._sse_closed = False + + self.set_header("Content-Type", "text/event-stream; charset=utf-8") + self.set_header("Cache-Control", "no-cache") + # Instruct nginx / other reverse proxies not to buffer this response. + self.set_header("X-Accel-Buffering", "no") + + buffer = get_client_buffer() if source == "client" else get_server_buffer() + + # Send backfill — all existing entries that pass the current filters. + for entry in _filter_entries( + buffer.get_entries(), level_name, search, username_filter, source + ): + self.write(f"data: {json.dumps(entry)}\n\n") + await self.flush() + + # Subscribe to future entries. + queue: asyncio.Queue = asyncio.Queue() + self._sse_queue = queue + + unsubscribe = buffer.subscribe(queue.put_nowait) + + try: + while True: + try: + entry = await asyncio.wait_for(queue.get(), timeout=20.0) + except asyncio.TimeoutError: + # Send a keepalive comment so proxies don't time out. + if self._sse_closed: + break + self.write(": keepalive\n\n") + await self.flush() + continue + + if entry is _STREAM_CLOSED: + break + + matched = _filter_entries( + [entry], level_name, search, username_filter, source + ) + if matched: + self.write(f"data: {json.dumps(matched[0])}\n\n") + await self.flush() + except Exception: # noqa: BLE001 + pass + finally: + unsubscribe() diff --git a/server/digi_server/app_server.py b/server/digi_server/app_server.py index 9e283908..6e6a9c54 100644 --- a/server/digi_server/app_server.py +++ b/server/digi_server/app_server.py @@ -18,9 +18,12 @@ from controllers import controllers from controllers.ws_controller import WebSocketController from digi_server.logger import ( + configure_client_buffer, + configure_client_logging, configure_db_logging, configure_file_logging, configure_log_level, + configure_server_buffer, get_logger, ) from digi_server.settings import Settings @@ -55,6 +58,9 @@ def __init__( self.digi_settings: Settings = Settings(self, settings_path) self.app_log_handler = None self.db_file_handler = None + self.client_file_handler = None + self.server_buffer = None + self.client_buffer = None # Controller imports (needed to trigger the decorator) controllers.import_all_controllers() @@ -377,6 +383,24 @@ async def _configure_logging(self): ) configure_log_level(log_level) + # Client logging + client_log_path = await self.digi_settings.get("client_log_path") + client_file_size = await self.digi_settings.get("client_max_log_mb") + client_backups = await self.digi_settings.get("client_log_backups") + client_log_level = await self.digi_settings.get("client_log_level") + self.client_file_handler = configure_client_logging( + log_path=client_log_path, + max_size_mb=client_file_size, + log_backups=client_backups, + handler=self.client_file_handler, + log_level=client_log_level, + ) + + # In-memory log buffers (for the log viewer UI) + buffer_maxlen = await self.digi_settings.get("log_buffer_size") + self.server_buffer = configure_server_buffer(buffer_maxlen) + self.client_buffer = configure_client_buffer(buffer_maxlen) + # Database logging use_db_logging = await self.digi_settings.get("db_log_enabled") db_log_path = await self.digi_settings.get("db_log_path") diff --git a/server/digi_server/log_buffer.py b/server/digi_server/log_buffer.py new file mode 100644 index 00000000..3f238978 --- /dev/null +++ b/server/digi_server/log_buffer.py @@ -0,0 +1,133 @@ +"""In-memory circular log buffer. + +Provides a :class:`LogBufferHandler` that stores the last N log records as +structured dicts in a :class:`collections.deque`. Two module-level singletons +are maintained — one for server-side logs and one for client-side logs — so +the log viewer endpoint can read from either without constructing new objects. + +Design rationale +---------------- +* Tornado is single-threaded; ``emit()`` is always called from the IOLoop + thread, so no mutex is required. +* ``deque(maxlen=N)`` provides O(1) append with automatic eviction of the + oldest entry when the buffer is full. +* ``list(deque)`` is GIL-atomic in CPython, making snapshot reads safe even + if a background thread were to emit a record concurrently. +""" + +import logging +from collections import deque +from datetime import datetime, timezone +from typing import Optional + + +class LogBufferHandler(logging.Handler): + """A logging handler that stores records in an in-memory circular buffer. + + :param maxlen: Maximum number of entries to keep. When full, the oldest + entry is evicted automatically. + """ + + def __init__(self, maxlen: int = 2000): + super().__init__() + self._buffer: deque = deque(maxlen=maxlen) + self._subscribers: set = set() + + def emit(self, record: logging.LogRecord) -> None: + """Build a structured dict from *record* and append it to the buffer. + + Extra attributes ``user_id``, ``username``, and ``remote_ip`` are read + via :func:`getattr` so they are optional; missing attributes become + ``None`` in the entry dict. + + :param record: The log record to store. + """ + try: + ts = datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat( + timespec="milliseconds" + ) + entry = { + "ts": ts, + "level": record.levelname, + "level_no": record.levelno, + "logger": record.name, + "message": record.getMessage(), + "filename": record.filename, + "lineno": record.lineno, + "user_id": getattr(record, "user_id", None), + "username": getattr(record, "username", None), + "remote_ip": getattr(record, "remote_ip", None), + } + self._buffer.append(entry) + for cb in list(self._subscribers): + try: + cb(entry) + except Exception: # noqa: BLE001 + pass + except Exception: # noqa: BLE001 + self.handleError(record) + + def get_entries(self) -> list: + """Return a snapshot of all buffered entries as a plain list. + + The returned list is independent of the internal deque; subsequent + ``emit()`` calls do not affect it. + + :returns: List of entry dicts ordered oldest-first. + """ + return list(self._buffer) + + def subscribe(self, callback) -> "callable": + """Register *callback* to be called with each new entry dict. + + The callback is invoked synchronously inside :meth:`emit`, on the + Tornado IOLoop thread, immediately after the entry is appended to the + buffer. Callbacks must be non-blocking. + + :param callback: A callable that accepts one positional argument (the + entry dict). + :returns: An unsubscribe callable. Call it to deregister *callback*. + """ + self._subscribers.add(callback) + + def _unsubscribe(): + self._subscribers.discard(callback) + + return _unsubscribe + + def resize(self, maxlen: int) -> None: + """Resize the buffer, preserving as many recent entries as possible. + + When shrinking, the *newest* ``maxlen`` entries are kept. + + :param maxlen: New maximum number of entries. + """ + new_buf: deque = deque(self._buffer, maxlen=maxlen) + self._buffer = new_buf + + +# Module-level singletons — one per log source. +_server_buffer: Optional[LogBufferHandler] = None +_client_buffer: Optional[LogBufferHandler] = None + + +def get_server_buffer() -> LogBufferHandler: + """Return (creating if necessary) the server-side log buffer singleton. + + :returns: The server :class:`LogBufferHandler` instance. + """ + global _server_buffer # noqa: PLW0603 + if _server_buffer is None: + _server_buffer = LogBufferHandler() + return _server_buffer + + +def get_client_buffer() -> LogBufferHandler: + """Return (creating if necessary) the client-side log buffer singleton. + + :returns: The client :class:`LogBufferHandler` instance. + """ + global _client_buffer # noqa: PLW0603 + if _client_buffer is None: + _client_buffer = LogBufferHandler() + return _client_buffer diff --git a/server/digi_server/logger.py b/server/digi_server/logger.py index d63a69e6..a5bd38a1 100644 --- a/server/digi_server/logger.py +++ b/server/digi_server/logger.py @@ -4,6 +4,12 @@ from tornado.log import LogFormatter +from digi_server.log_buffer import ( + LogBufferHandler, + get_client_buffer, + get_server_buffer, +) + logger = logging.getLogger("DigiScript") ALL_LOGGERS = [ @@ -78,6 +84,54 @@ def configure_db_logging( return file_handler +CLIENT_LEVEL_MAP = { + "TRACE": 5, # Registered via add_logging_level("TRACE", logging.DEBUG - 5) in main.py + "DEBUG": logging.DEBUG, + "INFO": logging.INFO, + "WARN": logging.WARNING, # loglevel npm uses WARN; Python uses WARNING + "ERROR": logging.ERROR, + "SILENT": logging.CRITICAL + 1, # No Python equivalent; suppress all +} + + +def map_client_level(level_name: str) -> int: + """Map a loglevel npm level name to a Python logging integer. + + :param level_name: Level name from the loglevel npm package (e.g. TRACE, WARN). + :returns: The corresponding Python logging integer level. + """ + return CLIENT_LEVEL_MAP.get(level_name.upper(), logging.INFO) + + +def configure_client_logging( + log_path, + max_size_mb=100, + log_backups=5, + handler=None, + log_level=logging.DEBUG, +): + size_bytes = max_size_mb * 1024 * 1024 + client_logger = get_logger("Client") + + if handler: + client_logger.removeHandler(handler) + + if isinstance(log_level, str): + log_level = map_client_level(log_level) + + client_logger.setLevel(log_level) + file_handler = None + if log_path: + file_handler = RotatingFileHandler( + log_path, maxBytes=size_bytes, backupCount=log_backups + ) + file_handler.setFormatter(LogFormatter(color=False)) + client_logger.addHandler(file_handler) + # Prevent propagation to avoid polluting the server console + client_logger.propagate = False + return file_handler + + def add_logging_level(level_name, level_num, method_name=None): if not method_name: method_name = level_name.lower() @@ -102,6 +156,40 @@ def log_to_root(message, *args, **kwargs): setattr(logging, method_name, log_to_root) +def configure_server_buffer(maxlen: int) -> LogBufferHandler: + """Attach (or resize) the server log buffer to all server loggers. + + Idempotent: if the handler is already attached, only the buffer size is + updated. + + :param maxlen: Maximum number of entries to keep in the buffer. + :returns: The :class:`LogBufferHandler` singleton for server logs. + """ + handler = get_server_buffer() + handler.resize(maxlen) + for _logger in ALL_LOGGERS: + if handler not in _logger.handlers: + _logger.addHandler(handler) + return handler + + +def configure_client_buffer(maxlen: int) -> LogBufferHandler: + """Attach (or resize) the client log buffer to the DigiScript.Client logger. + + Idempotent: if the handler is already attached, only the buffer size is + updated. + + :param maxlen: Maximum number of entries to keep in the buffer. + :returns: The :class:`LogBufferHandler` singleton for client logs. + """ + handler = get_client_buffer() + handler.resize(maxlen) + client_logger = get_logger("Client") + if handler not in client_logger.handlers: + client_logger.addHandler(handler) + return handler + + def get_level_names_by_order(): levels = logging.getLevelNamesMapping() sorted_levels = sorted(levels.items(), key=lambda x: x[1]) diff --git a/server/digi_server/settings.py b/server/digi_server/settings.py index dfe386d4..b23b49d6 100644 --- a/server/digi_server/settings.py +++ b/server/digi_server/settings.py @@ -279,6 +279,61 @@ def __init__(self, application: DigiScriptServer, settings_path=None): display_name="Enable Network Discovery (mDNS)", help_text="Advertise this server on the local network for automatic discovery by desktop clients.", ) + self.define( + "client_log_enabled", + bool, + True, + True, + display_name="Enable Client Log Forwarding", + help_text="When enabled, client browsers will forward their logs to the server.", + ) + self.define( + "client_log_level", + str, + "INFO", + True, + self._application.regen_logging, + display_name="Client Log Level", + help_text="Minimum log level that clients will forward to the server.", + choice_options=["TRACE", "DEBUG", "INFO", "WARN", "ERROR"], + ) + self.define( + "client_log_path", + str, + os.path.join(self._base_path, "digiscript_client.log"), + True, + self._application.regen_logging, + display_name="Client Log Path", + help_text="Path to the log file for client-side log messages.", + ) + self.define( + "client_max_log_mb", + int, + 100, + True, + self._application.regen_logging, + display_name="Max Client Log Size (MB)", + help_text="Maximum size in MB of the client log file before it is rotated.", + ) + self.define( + "client_log_backups", + int, + 5, + True, + self._application.regen_logging, + display_name="Client Log Backups", + help_text="Number of rotated client log file backups to retain.", + ) + self.define( + "log_buffer_size", + int, + 2000, + True, + self._application.regen_logging, + display_name="Log Buffer Size", + help_text="Number of recent log entries to keep in memory for the log viewer. " + "Larger values use more memory. Changes take effect after restart.", + ) self._load(spawn_callbacks=False) diff --git a/server/models/user.py b/server/models/user.py index aae2b45f..258655c9 100644 --- a/server/models/user.py +++ b/server/models/user.py @@ -6,7 +6,7 @@ from functools import partial from typing import TYPE_CHECKING, List, Union -from sqlalchemy import ForeignKey, Integer, Text, TypeDecorator, select +from sqlalchemy import CheckConstraint, ForeignKey, Integer, Text, TypeDecorator, select from sqlalchemy.orm import Mapped, mapped_column, relationship from models.models import db @@ -86,6 +86,14 @@ class UserSettings(db.Model): script_text_alignment: Mapped[TextAlignment] = mapped_column( TextAlignmentCol, default=TextAlignment.CENTER ) + console_log_level: Mapped[str] = mapped_column(default="WARN") + + __table_args__ = ( + CheckConstraint( + "console_log_level IN ('TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'SILENT')", + name="ck_user_settings_console_log_level", + ), + ) # Hidden Properties (None user editable, marked with _) # Make sure to also mark these as hidden in the Schema for this in schemas/schemas.py diff --git a/server/test/controllers/api/test_logging.py b/server/test/controllers/api/test_logging.py new file mode 100644 index 00000000..905ed3ed --- /dev/null +++ b/server/test/controllers/api/test_logging.py @@ -0,0 +1,72 @@ +import json + +from tornado import escape +from tornado.testing import gen_test + +from test.conftest import DigiScriptTestCase + + +class TestLoggingController(DigiScriptTestCase): + @gen_test + async def test_logging_endpoint_success(self): + """Test that the logging endpoint successfully receives logs.""" + payload = { + "level": "INFO", + "message": "Test message from client", + "extra": {"source": "unit-test"}, + } + response = await self.http_client.fetch( + self.get_url("/api/v1/logs"), + method="POST", + body=json.dumps(payload), + headers={"Content-Type": "application/json"}, + ) + self.assertEqual(200, response.code) + data = escape.json_decode(response.body) + self.assertEqual("OK", data["status"]) + + @gen_test + async def test_logging_endpoint_disabled(self): + """Test that the logging endpoint returns 403 when disabled in settings.""" + # Disable client logging in settings + await self._app.digi_settings.set("client_log_enabled", False) + + payload = {"level": "INFO", "message": "Test message from client"} + response = await self.http_client.fetch( + self.get_url("/api/v1/logs"), + method="POST", + body=json.dumps(payload), + headers={"Content-Type": "application/json"}, + raise_error=False, + ) + self.assertEqual(403, response.code) + data = escape.json_decode(response.body) + self.assertEqual("Client logging is disabled", data["message"]) + + # Re-enable for other tests + await self._app.digi_settings.set("client_log_enabled", True) + + @gen_test + async def test_logging_endpoint_invalid_json(self): + """Test that the logging endpoint returns 400 for invalid JSON.""" + response = await self.http_client.fetch( + self.get_url("/api/v1/logs"), + method="POST", + body="invalid json", + headers={"Content-Type": "application/json"}, + raise_error=False, + ) + self.assertEqual(400, response.code) + + @gen_test + async def test_logging_endpoint_different_levels(self): + """Test that the logging endpoint accepts various log levels.""" + for level in ["DEBUG", "INFO", "WARN", "ERROR"]: + payload = {"level": level, "message": f"Test level {level}"} + response = await self.http_client.fetch( + self.get_url("/api/v1/logs"), + method="POST", + body=json.dumps(payload), + headers={"Content-Type": "application/json"}, + ) + self.assertEqual(200, response.code) diff --git a/server/test/controllers/api/test_logs_viewer.py b/server/test/controllers/api/test_logs_viewer.py new file mode 100644 index 00000000..bf68508a --- /dev/null +++ b/server/test/controllers/api/test_logs_viewer.py @@ -0,0 +1,396 @@ +"""Integration tests for GET /api/v1/logs/view.""" + +import logging + +from tornado import escape + +from digi_server.log_buffer import get_client_buffer, get_server_buffer +from test.conftest import DigiScriptTestCase + + +def _make_record(msg, level=logging.INFO, name="test", **extra_attrs): + record = logging.LogRecord( + name=name, + level=level, + pathname="fake.py", + lineno=1, + msg=msg, + args=(), + exc_info=None, + ) + for k, v in extra_attrs.items(): + setattr(record, k, v) + return record + + +class TestLogViewerController(DigiScriptTestCase): + """Tests for the log viewer endpoint.""" + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + def _create_and_login_admin(self, username="admin", password="adminpass"): + self.fetch( + "/api/v1/auth/create", + method="POST", + body=escape.json_encode( + {"username": username, "password": password, "is_admin": True} + ), + ) + resp = self.fetch( + "/api/v1/auth/login", + method="POST", + body=escape.json_encode({"username": username, "password": password}), + ) + return escape.json_decode(resp.body)["access_token"] + + def _create_and_login_user(self, admin_token, username="user", password="userpass"): + self.fetch( + "/api/v1/auth/create", + method="POST", + body=escape.json_encode( + {"username": username, "password": password, "is_admin": False} + ), + headers={"Authorization": f"Bearer {admin_token}"}, + ) + resp = self.fetch( + "/api/v1/auth/login", + method="POST", + body=escape.json_encode({"username": username, "password": password}), + ) + return escape.json_decode(resp.body)["access_token"] + + def _inject_server_entry(self, msg, level=logging.INFO): + get_server_buffer().emit(_make_record(msg, level=level)) + + def _inject_client_entry(self, msg, level=logging.INFO, **extra): + get_client_buffer().emit(_make_record(msg, level=level, **extra)) + + def _fetch_view(self, token=None, **params): + qs = "&".join(f"{k}={v}" for k, v in params.items()) + url = f"/api/v1/logs/view?{qs}" if qs else "/api/v1/logs/view" + headers = {} + if token: + headers["Authorization"] = f"Bearer {token}" + return self.fetch(url, method="GET", headers=headers, raise_error=False) + + # ------------------------------------------------------------------ + # Authentication / authorisation + # ------------------------------------------------------------------ + + def test_unauthenticated_returns_401(self): + resp = self._fetch_view() + self.assertEqual(401, resp.code) + + def test_non_admin_returns_401(self): + admin_token = self._create_and_login_admin() + user_token = self._create_and_login_user(admin_token) + resp = self._fetch_view(token=user_token) + self.assertEqual(401, resp.code) + + def test_admin_returns_200(self): + token = self._create_and_login_admin() + resp = self._fetch_view(token=token) + self.assertEqual(200, resp.code) + + def test_admin_response_structure(self): + """Response must contain entries, total, returned, source.""" + token = self._create_and_login_admin() + resp = self._fetch_view(token=token) + body = escape.json_decode(resp.body) + for key in ("entries", "total", "returned", "source"): + self.assertIn(key, body, f"Missing key: {key}") + + # ------------------------------------------------------------------ + # Source selection + # ------------------------------------------------------------------ + + def test_source_defaults_to_server(self): + token = self._create_and_login_admin() + resp = self._fetch_view(token=token) + body = escape.json_decode(resp.body) + self.assertEqual("server", body["source"]) + + def test_source_server_explicit(self): + token = self._create_and_login_admin() + resp = self._fetch_view(token=token, source="server") + body = escape.json_decode(resp.body) + self.assertEqual("server", body["source"]) + + def test_source_client(self): + token = self._create_and_login_admin() + resp = self._fetch_view(token=token, source="client") + body = escape.json_decode(resp.body) + self.assertEqual("client", body["source"]) + + def test_server_and_client_buffers_are_independent(self): + """Entries injected into server buffer must not appear in client view.""" + self._inject_server_entry("server_only_msg") + token = self._create_and_login_admin() + + server_resp = escape.json_decode( + self._fetch_view(token=token, source="server").body + ) + client_resp = escape.json_decode( + self._fetch_view(token=token, source="client").body + ) + + server_msgs = [e["message"] for e in server_resp["entries"]] + client_msgs = [e["message"] for e in client_resp["entries"]] + + self.assertIn("server_only_msg", server_msgs) + self.assertNotIn("server_only_msg", client_msgs) + + # ------------------------------------------------------------------ + # Level filter + # ------------------------------------------------------------------ + + def test_level_filter_excludes_lower_levels(self): + """Requesting ERROR+ should hide INFO entries.""" + self._inject_server_entry("info_entry", level=logging.INFO) + self._inject_server_entry("error_entry", level=logging.ERROR) + token = self._create_and_login_admin() + + resp = escape.json_decode( + self._fetch_view(token=token, source="server", level="ERROR").body + ) + messages = [e["message"] for e in resp["entries"]] + self.assertIn("error_entry", messages) + self.assertNotIn("info_entry", messages) + + def test_level_filter_empty_returns_all(self): + """Empty level param returns entries at all levels.""" + self._inject_server_entry("debug_entry", level=logging.DEBUG) + self._inject_server_entry("critical_entry", level=logging.CRITICAL) + token = self._create_and_login_admin() + + resp = escape.json_decode( + self._fetch_view(token=token, source="server", level="").body + ) + messages = [e["message"] for e in resp["entries"]] + self.assertIn("debug_entry", messages) + self.assertIn("critical_entry", messages) + + def test_warn_alias(self): + """level=WARN should behave identically to level=WARNING.""" + self._inject_server_entry("warn_entry", level=logging.WARNING) + self._inject_server_entry("debug_entry_warn_alias", level=logging.DEBUG) + token = self._create_and_login_admin() + + warn_resp = escape.json_decode( + self._fetch_view(token=token, source="server", level="WARN").body + ) + warning_resp = escape.json_decode( + self._fetch_view(token=token, source="server", level="WARNING").body + ) + + warn_msgs = [e["message"] for e in warn_resp["entries"]] + warning_msgs = [e["message"] for e in warning_resp["entries"]] + + self.assertIn("warn_entry", warn_msgs) + self.assertNotIn("debug_entry_warn_alias", warn_msgs) + self.assertEqual(set(warn_msgs), set(warning_msgs)) + + # ------------------------------------------------------------------ + # Search filter + # ------------------------------------------------------------------ + + def test_search_filter(self): + self._inject_server_entry("unique_search_term_xyz found here") + self._inject_server_entry("unrelated log entry") + token = self._create_and_login_admin() + + resp = escape.json_decode( + self._fetch_view( + token=token, source="server", search="unique_search_term_xyz" + ).body + ) + messages = [e["message"] for e in resp["entries"]] + self.assertTrue(any("unique_search_term_xyz" in m for m in messages)) + self.assertFalse(any("unrelated log entry" in m for m in messages)) + + def test_search_filter_case_insensitive(self): + self._inject_server_entry("CaseSensitiveTest message") + token = self._create_and_login_admin() + + resp = escape.json_decode( + self._fetch_view( + token=token, source="server", search="casesensitivetest" + ).body + ) + messages = [e["message"] for e in resp["entries"]] + self.assertTrue(any("CaseSensitiveTest" in m for m in messages)) + + # ------------------------------------------------------------------ + # Username filter (client source only) + # ------------------------------------------------------------------ + + def test_username_filter_client_source(self): + self._inject_client_entry("alice log", username="alice") + self._inject_client_entry("bob log", username="bob") + token = self._create_and_login_admin() + + resp = escape.json_decode( + self._fetch_view(token=token, source="client", username="alice").body + ) + messages = [e["message"] for e in resp["entries"]] + self.assertIn("alice log", messages) + self.assertNotIn("bob log", messages) + + def test_username_filter_ignored_for_server_source(self): + """username param should have no effect on the server source.""" + self._inject_server_entry("server_entry_for_username_test") + token = self._create_and_login_admin() + + resp_no_filter = escape.json_decode( + self._fetch_view(token=token, source="server").body + ) + resp_with_filter = escape.json_decode( + self._fetch_view(token=token, source="server", username="alice").body + ) + self.assertEqual(resp_no_filter["total"], resp_with_filter["total"]) + + # ------------------------------------------------------------------ + # Pagination + # ------------------------------------------------------------------ + + def test_limit(self): + for i in range(20): + self._inject_server_entry(f"limit_test_entry {i}") + token = self._create_and_login_admin() + + resp = escape.json_decode( + self._fetch_view(token=token, source="server", limit=5).body + ) + self.assertLessEqual(len(resp["entries"]), 5) + self.assertEqual(len(resp["entries"]), resp["returned"]) + + def test_offset(self): + """offset should skip entries.""" + # Inject 10 entries that are easy to identify + for i in range(10): + self._inject_server_entry(f"offset_test_msg {i}") + token = self._create_and_login_admin() + + resp_all = escape.json_decode( + self._fetch_view( + token=token, source="server", search="offset_test_msg", limit=10 + ).body + ) + resp_offset = escape.json_decode( + self._fetch_view( + token=token, + source="server", + search="offset_test_msg", + limit=10, + offset=5, + ).body + ) + # total should be the same regardless of offset + self.assertEqual(resp_all["total"], resp_offset["total"]) + # offset result should have 5 fewer entries + self.assertEqual(resp_all["returned"] - 5, resp_offset["returned"]) + + def test_limit_capped_at_1000(self): + """limit > 1000 should be silently capped, not error.""" + token = self._create_and_login_admin() + # Just verify a large limit doesn't cause an error + self.assertEqual( + 200, + self.fetch( + "/api/v1/logs/view?limit=9999", + method="GET", + headers={"Authorization": f"Bearer {token}"}, + raise_error=False, + ).code, + ) + + # ------------------------------------------------------------------ + # total vs returned + # ------------------------------------------------------------------ + + def test_total_reflects_filtered_count(self): + """total should count all matching entries, not just the page.""" + for i in range(15): + self._inject_server_entry(f"total_test_entry {i}") + token = self._create_and_login_admin() + + resp = escape.json_decode( + self._fetch_view( + token=token, source="server", search="total_test_entry", limit=5 + ).body + ) + self.assertEqual(15, resp["total"]) + self.assertEqual(5, resp["returned"]) + + # ------------------------------------------------------------------ + # SSE stream endpoint — auth and header checks + # ------------------------------------------------------------------ + # Full streaming content tests require async tooling; these tests verify + # the auth layer before any streaming begins. + + def _fetch_stream_headers(self, token=None, **params): + """Perform a GET /api/v1/logs/stream with a very short request timeout. + + The stream never sends a final Content-Length, so self.fetch() would + hang indefinitely without a timeout. We use request_timeout=0.5 s; + the server returns an HTTP 401/403 immediately for auth failures, but + for a valid admin request the timeout fires after headers are received + and some initial data may have been sent. + """ + qs = "&".join(f"{k}={v}" for k, v in params.items()) + url = f"/api/v1/logs/stream?{qs}" if qs else "/api/v1/logs/stream" + headers = {} + if token: + headers["Authorization"] = f"Bearer {token}" + return self.fetch( + url, + method="GET", + headers=headers, + raise_error=False, + request_timeout=0.5, + ) + + def test_stream_unauthenticated_returns_401(self): + resp = self._fetch_stream_headers() + self.assertEqual(401, resp.code) + + def test_stream_non_admin_returns_401(self): + admin_token = self._create_and_login_admin() + user_token = self._create_and_login_user(admin_token) + resp = self._fetch_stream_headers(token=user_token) + self.assertEqual(401, resp.code) + + def test_stream_backfill_contains_existing_entries(self): + """Entries injected before connection must appear in the first chunk. + + SSE connections never send a terminal response, so self.fetch() raises + HTTPTimeoutError when the request_timeout expires. We use + streaming_callback to collect chunks as they arrive and inspect the + accumulated body in the except handler. + """ + self._inject_server_entry("sse_backfill_unique_xyz") + token = self._create_and_login_admin() + + received_chunks = [] + + try: + self.fetch( + "/api/v1/logs/stream?source=server", + method="GET", + headers={"Authorization": f"Bearer {token}"}, + raise_error=False, + request_timeout=1.0, + streaming_callback=received_chunks.append, + ) + except Exception: + # HTTPTimeoutError is expected — the stream stays open indefinitely. + pass + + self.assertTrue(len(received_chunks) > 0, "No data received from SSE stream") + full_body = b"".join(received_chunks).decode() + self.assertIn("sse_backfill_unique_xyz", full_body) + # Verify SSE wire format: events must begin with "data: " + data_lines = [ln for ln in full_body.splitlines() if ln.startswith("data: ")] + self.assertTrue(len(data_lines) > 0) diff --git a/server/test/digi_server/test_log_buffer.py b/server/test/digi_server/test_log_buffer.py new file mode 100644 index 00000000..08c94a23 --- /dev/null +++ b/server/test/digi_server/test_log_buffer.py @@ -0,0 +1,238 @@ +import logging +import unittest +from datetime import datetime + +from digi_server.log_buffer import ( + LogBufferHandler, + get_client_buffer, + get_server_buffer, +) + + +def _make_record(msg="hello", level=logging.INFO, name="test", **extra_attrs): + """Helper: create a :class:`logging.LogRecord` with optional extra attrs.""" + record = logging.LogRecord( + name=name, + level=level, + pathname="test_file.py", + lineno=42, + msg=msg, + args=(), + exc_info=None, + ) + for key, value in extra_attrs.items(): + setattr(record, key, value) + return record + + +class TestLogBufferHandler(unittest.TestCase): + def setUp(self): + self.handler = LogBufferHandler(maxlen=100) + + # ------------------------------------------------------------------ + # Basic emit / retrieval + # ------------------------------------------------------------------ + + def test_captures_entry(self): + """An emitted record should appear in get_entries().""" + self.handler.emit(_make_record("test message")) + entries = self.handler.get_entries() + self.assertEqual(1, len(entries)) + self.assertEqual("test message", entries[0]["message"]) + + def test_entry_schema(self): + """Every required key must be present in an emitted entry.""" + self.handler.emit( + _make_record("schema check", level=logging.WARNING, name="DigiScript") + ) + entry = self.handler.get_entries()[0] + for key in ( + "ts", + "level", + "level_no", + "logger", + "message", + "filename", + "lineno", + ): + self.assertIn(key, entry, f"Missing key: {key}") + self.assertEqual("WARNING", entry["level"]) + self.assertEqual(logging.WARNING, entry["level_no"]) + self.assertEqual("DigiScript", entry["logger"]) + self.assertEqual("schema check", entry["message"]) + + def test_empty_buffer_returns_empty_list(self): + self.assertEqual([], self.handler.get_entries()) + + # ------------------------------------------------------------------ + # Circular buffer (maxlen eviction) + # ------------------------------------------------------------------ + + def test_maxlen_eviction(self): + """When the buffer is full, the oldest entry is dropped.""" + handler = LogBufferHandler(maxlen=5) + for i in range(6): + handler.emit(_make_record(f"msg {i}")) + entries = handler.get_entries() + self.assertEqual(5, len(entries)) + # Oldest entry (msg 0) must have been evicted + messages = [e["message"] for e in entries] + self.assertNotIn("msg 0", messages) + self.assertIn("msg 5", messages) + + def test_maxlen_not_exceeded(self): + handler = LogBufferHandler(maxlen=10) + for i in range(10): + handler.emit(_make_record(f"msg {i}")) + self.assertEqual(10, len(handler.get_entries())) + + # ------------------------------------------------------------------ + # Resize + # ------------------------------------------------------------------ + + def test_resize_shrink_keeps_newest(self): + """Shrinking retains the newest entries.""" + handler = LogBufferHandler(maxlen=100) + for i in range(100): + handler.emit(_make_record(f"msg {i}")) + handler.resize(50) + entries = handler.get_entries() + self.assertEqual(50, len(entries)) + # Newest entry must still be present + self.assertEqual("msg 99", entries[-1]["message"]) + + def test_resize_grow_retains_all(self): + """Growing preserves all existing entries.""" + handler = LogBufferHandler(maxlen=10) + for i in range(10): + handler.emit(_make_record(f"msg {i}")) + handler.resize(200) + self.assertEqual(10, len(handler.get_entries())) + + def test_resize_same_size(self): + """Resizing to the current size should be a no-op for content.""" + handler = LogBufferHandler(maxlen=5) + for i in range(5): + handler.emit(_make_record(f"msg {i}")) + handler.resize(5) + self.assertEqual(5, len(handler.get_entries())) + + # ------------------------------------------------------------------ + # Snapshot isolation + # ------------------------------------------------------------------ + + def test_get_entries_snapshot(self): + """The returned list is independent of subsequent emits.""" + self.handler.emit(_make_record("first")) + snapshot = self.handler.get_entries() + self.handler.emit(_make_record("second")) + # Snapshot should still only contain the first entry + self.assertEqual(1, len(snapshot)) + + # ------------------------------------------------------------------ + # Client extra fields + # ------------------------------------------------------------------ + + def test_client_extra_fields_present(self): + """Extra attrs user_id / username / remote_ip are stored in the entry.""" + record = _make_record( + "client log", + user_id=7, + username="alice", + remote_ip="192.168.1.1", + ) + self.handler.emit(record) + entry = self.handler.get_entries()[0] + self.assertEqual(7, entry["user_id"]) + self.assertEqual("alice", entry["username"]) + self.assertEqual("192.168.1.1", entry["remote_ip"]) + + def test_missing_extra_fields_are_none(self): + """Records without client extra fields should store None, not raise.""" + self.handler.emit(_make_record("plain record")) + entry = self.handler.get_entries()[0] + self.assertIsNone(entry["user_id"]) + self.assertIsNone(entry["username"]) + self.assertIsNone(entry["remote_ip"]) + + # ------------------------------------------------------------------ + # Singletons + # ------------------------------------------------------------------ + + def test_get_server_buffer_singleton(self): + """get_server_buffer() returns the same instance on repeated calls.""" + a = get_server_buffer() + b = get_server_buffer() + self.assertIs(a, b) + + def test_get_client_buffer_singleton(self): + """get_client_buffer() returns the same instance on repeated calls.""" + a = get_client_buffer() + b = get_client_buffer() + self.assertIs(a, b) + + def test_server_and_client_buffers_are_distinct(self): + """Server and client buffers must be separate objects.""" + self.assertIsNot(get_server_buffer(), get_client_buffer()) + + # ------------------------------------------------------------------ + # Pub/sub (subscribe / unsubscribe) + # ------------------------------------------------------------------ + + def test_subscribe_called_on_emit(self): + """A registered callback is invoked when a record is emitted.""" + received = [] + self.handler.subscribe(received.append) + self.handler.emit(_make_record("sub test")) + self.assertEqual(1, len(received)) + self.assertEqual("sub test", received[0]["message"]) + + def test_subscribe_receives_correct_entry(self): + """The callback receives the same dict that appears in get_entries().""" + received = [] + self.handler.subscribe(received.append) + self.handler.emit(_make_record("match test", level=logging.ERROR)) + self.assertEqual(received[0], self.handler.get_entries()[-1]) + + def test_unsubscribe_stops_callbacks(self): + """Calling the returned unsubscribe callable stops future callbacks.""" + received = [] + unsubscribe = self.handler.subscribe(received.append) + self.handler.emit(_make_record("before")) + unsubscribe() + self.handler.emit(_make_record("after")) + # Only the first emit should have reached the callback. + self.assertEqual(1, len(received)) + self.assertEqual("before", received[0]["message"]) + + def test_multiple_subscribers_all_notified(self): + """All registered callbacks receive each emitted entry.""" + received_a, received_b = [], [] + self.handler.subscribe(received_a.append) + self.handler.subscribe(received_b.append) + self.handler.emit(_make_record("multi")) + self.assertEqual(1, len(received_a)) + self.assertEqual(1, len(received_b)) + + def test_subscriber_exception_does_not_break_emit(self): + """A callback that raises must not prevent the entry being buffered.""" + + def bad_callback(_entry): + raise RuntimeError("boom") + + self.handler.subscribe(bad_callback) + # Should not raise — the exception is caught internally. + self.handler.emit(_make_record("resilient")) + self.assertEqual(1, len(self.handler.get_entries())) + + # ------------------------------------------------------------------ + # Timestamp format + # ------------------------------------------------------------------ + + def test_timestamp_is_iso8601(self): + """The ts field should be parseable as an ISO-8601 UTC timestamp.""" + self.handler.emit(_make_record("ts test")) + ts = self.handler.get_entries()[0]["ts"] + # Should not raise + parsed = datetime.fromisoformat(ts) + self.assertIsNotNone(parsed) diff --git a/server/test/digi_server/test_logger.py b/server/test/digi_server/test_logger.py new file mode 100644 index 00000000..617be374 --- /dev/null +++ b/server/test/digi_server/test_logger.py @@ -0,0 +1,40 @@ +import logging +import unittest + +from digi_server.logger import CLIENT_LEVEL_MAP, map_client_level + + +class TestMapClientLevel(unittest.TestCase): + def test_trace_maps_to_level_5(self): + self.assertEqual(5, map_client_level("TRACE")) + + def test_debug_maps_to_logging_debug(self): + self.assertEqual(logging.DEBUG, map_client_level("DEBUG")) + + def test_info_maps_to_logging_info(self): + self.assertEqual(logging.INFO, map_client_level("INFO")) + + def test_warn_maps_to_logging_warning(self): + """loglevel npm uses WARN; Python uses WARNING (30).""" + self.assertEqual(logging.WARNING, map_client_level("WARN")) + + def test_error_maps_to_logging_error(self): + self.assertEqual(logging.ERROR, map_client_level("ERROR")) + + def test_silent_maps_above_critical(self): + """SILENT has no Python equivalent; should suppress all output.""" + self.assertEqual(logging.CRITICAL + 1, map_client_level("SILENT")) + + def test_unknown_level_falls_back_to_info(self): + self.assertEqual(logging.INFO, map_client_level("UNKNOWN")) + self.assertEqual(logging.INFO, map_client_level("")) + self.assertEqual(logging.INFO, map_client_level("NOTSET")) + + def test_case_insensitive(self): + self.assertEqual(logging.WARNING, map_client_level("warn")) + self.assertEqual(logging.INFO, map_client_level("info")) + self.assertEqual(5, map_client_level("trace")) + + def test_client_level_map_contains_all_expected_keys(self): + expected = {"TRACE", "DEBUG", "INFO", "WARN", "ERROR", "SILENT"} + self.assertEqual(expected, set(CLIENT_LEVEL_MAP.keys())) diff --git a/server/utils/web/base_controller.py b/server/utils/web/base_controller.py index 22a04e2f..8fff65c8 100644 --- a/server/utils/web/base_controller.py +++ b/server/utils/web/base_controller.py @@ -171,9 +171,16 @@ def _unimplemented_method(self, *args: str, **kwargs: str) -> None: self.write({"message": "405 not allowed"}) def on_finish(self): + from utils.web.route import Route # noqa: PLC0415 + + if self.request.path in Route.ignored_logging_routes(): + log_method = get_logger().trace + else: + log_method = get_logger().debug + if self.request.body: try: - get_logger().debug( + log_method( f"{self.request.method} " f"{self.request.path} " f"{escape.json_decode(self.request.body)}" diff --git a/server/utils/web/route.py b/server/utils/web/route.py index 2bdb7bfd..464b455d 100644 --- a/server/utils/web/route.py +++ b/server/utils/web/route.py @@ -56,9 +56,15 @@ class ApiVersion(Enum): class ApiRoute(Route): - def __init__(self, route: str, api_version: ApiVersion, name: str = None): + def __init__( + self, + route: str, + api_version: ApiVersion, + name: str = None, + ignore_logging: bool = False, + ): route = f"/api/v{api_version.value}/{route.removeprefix('/')}" - super().__init__(route, name) + super().__init__(route, name, ignore_logging) def __call__(self, controller): if not issubclass(controller, (BaseAPIController, WebSocketHandler)): From 14499eef3a0c92b8c9f19b92f33deb785ece09bf Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sat, 21 Feb 2026 01:00:34 +0000 Subject: [PATCH 36/43] Disable logging from websocket sessions controller --- server/controllers/api/websocket.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/controllers/api/websocket.py b/server/controllers/api/websocket.py index 2588bc34..521b49ff 100644 --- a/server/controllers/api/websocket.py +++ b/server/controllers/api/websocket.py @@ -6,7 +6,7 @@ from utils.web.route import ApiRoute, ApiVersion -@ApiRoute("ws/sessions", ApiVersion.V1) +@ApiRoute("ws/sessions", ApiVersion.V1, ignore_logging=True) class WebsocketSessionsController(BaseAPIController): def get(self): session_scheme = SessionSchema() From b82c9e95135e11c3399325af32f0a7cc77118a3d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 11:45:18 +0000 Subject: [PATCH 37/43] Bump ruff from 0.15.1 to 0.15.2 in /server (#926) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.1 to 0.15.2. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.15.1...0.15.2) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.15.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- server/test_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/test_requirements.txt b/server/test_requirements.txt index cf8a867c..6b5ae44f 100644 --- a/server/test_requirements.txt +++ b/server/test_requirements.txt @@ -1,3 +1,3 @@ pytest<9.1 pytest-asyncio>=1.3.0 -ruff==0.15.1 \ No newline at end of file +ruff==0.15.2 \ No newline at end of file From d352e9b40f70cca749610cf1d1b00dc4bf9d8a69 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Mon, 23 Feb 2026 19:16:22 +0000 Subject: [PATCH 38/43] Add log redaction setting and config to user routes (#933) --- server/controllers/api/auth/user.py | 4 ++++ server/digi_server/settings.py | 8 ++++++++ server/requirements.txt | 3 ++- server/utils/web/base_controller.py | 22 +++++++++++++++++----- server/utils/web/web_decorators.py | 27 ++++++++++++++++++++++++++- 5 files changed, 57 insertions(+), 7 deletions(-) diff --git a/server/controllers/api/auth/user.py b/server/controllers/api/auth/user.py index 6481f3f0..d7248d23 100644 --- a/server/controllers/api/auth/user.py +++ b/server/controllers/api/auth/user.py @@ -14,12 +14,14 @@ allow_when_password_required, api_authenticated, no_live_session, + redact_data_paths, require_admin, ) @ApiRoute("auth/create", ApiVersion.V1) class UserCreateController(BaseAPIController): + @redact_data_paths(paths=["/password", "/confirmPassword"]) async def post(self): with self.make_session() as session: # If there are no users, allow creation without authentication, otherwise require admin. @@ -139,6 +141,7 @@ async def post(self): @ApiRoute("auth/login", ApiVersion.V1) class LoginHandler(BaseAPIController): + @redact_data_paths(paths=["/password"]) async def post(self): data = escape.json_decode(self.request.body) @@ -274,6 +277,7 @@ class PasswordChangeController(BaseAPIController): @api_authenticated @allow_when_password_required + @redact_data_paths(paths=["/old_password", "/new_password"]) async def patch(self): """ Change authenticated user's password. diff --git a/server/digi_server/settings.py b/server/digi_server/settings.py index b23b49d6..2f582c50 100644 --- a/server/digi_server/settings.py +++ b/server/digi_server/settings.py @@ -230,6 +230,14 @@ def __init__(self, application: DigiScriptServer, settings_path=None): self._application.regen_logging, display_name="Log Backups", ) + self.define( + "log_redaction", + bool, + False, + True, + display_name="Enable Log Redaction", + help_text="When enabled, potentially sensitive information will be redacted from logs.", + ) self.define( "db_log_enabled", bool, diff --git a/server/requirements.txt b/server/requirements.txt index cb7fec8a..51709168 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -11,4 +11,5 @@ marshmallow<5 pyjwt[crypto]==2.11.0 setuptools==80.10.2 xkcdpass==1.30.0 -zeroconf==0.148.0 \ No newline at end of file +zeroconf==0.148.0 +python-jsonpath==2.0.2 \ No newline at end of file diff --git a/server/utils/web/base_controller.py b/server/utils/web/base_controller.py index 8fff65c8..3ecd84f7 100644 --- a/server/utils/web/base_controller.py +++ b/server/utils/web/base_controller.py @@ -1,5 +1,6 @@ from __future__ import annotations +from copy import deepcopy from typing import TYPE_CHECKING, Any, Awaitable, Optional import bcrypt @@ -179,14 +180,25 @@ def on_finish(self): log_method = get_logger().debug if self.request.body: + method_name = self.request.method.lower() + handler_method = getattr(self, method_name, None) + redacted_data_paths = getattr(handler_method, "_redacted_data_paths", None) try: - log_method( - f"{self.request.method} " - f"{self.request.path} " - f"{escape.json_decode(self.request.body)}" - ) + body = escape.json_decode(self.request.body) except BaseException: get_logger().debug( f"{self.request.method} {self.request.path} {self.request.body}" ) + else: + if ( + redacted_data_paths + and self.application.digi_settings.settings[ + "log_redaction" + ].get_value() + ): + body = deepcopy(body) + redacted_data_paths.apply(body) + + log_method(f"{self.request.method} {self.request.path} {body}") + super().on_finish() diff --git a/server/utils/web/web_decorators.py b/server/utils/web/web_decorators.py index bdd9d877..0ba3862d 100644 --- a/server/utils/web/web_decorators.py +++ b/server/utils/web/web_decorators.py @@ -1,6 +1,7 @@ import functools -from typing import Awaitable, Callable, Optional +from typing import Awaitable, Callable, List, Optional +from jsonpath import JSONPatch from tornado.web import HTTPError from utils.web.base_controller import BaseController @@ -77,3 +78,27 @@ def wrapper(self: BaseController, *args, **kwargs) -> Optional[Awaitable[None]]: # Mark the wrapper with an attribute so prepare() can detect it wrapper._allow_when_password_required = True # type: ignore return wrapper + + +def redact_data_paths( + paths: List[str], +) -> Callable[ + [Callable[..., Optional[Awaitable[None]]]], Callable[..., Optional[Awaitable[None]]] +]: + patch = None + if paths: + patch = JSONPatch() + for path in paths: + patch.replace(path, "<-- REDACTED -->") + + def decorator( + method: Callable[..., Optional[Awaitable[None]]], + ) -> Callable[..., Optional[Awaitable[None]]]: + @functools.wraps(method) + def wrapper(self: BaseController, *args, **kwargs) -> Optional[Awaitable[None]]: + return method(self, *args, **kwargs) + + wrapper._redacted_data_paths = patch + return wrapper + + return decorator From 30c0e500ae60ba22654ed4b50f37f31f1b549abd Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Mon, 23 Feb 2026 19:21:34 +0000 Subject: [PATCH 39/43] Refactor log buffer into utils package --- server/controllers/api/logs_viewer.py | 2 +- server/digi_server/logger.py | 2 +- server/test/controllers/api/test_logs_viewer.py | 2 +- server/test/digi_server/test_log_buffer.py | 2 +- server/{digi_server => utils}/log_buffer.py | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename server/{digi_server => utils}/log_buffer.py (100%) diff --git a/server/controllers/api/logs_viewer.py b/server/controllers/api/logs_viewer.py index d934548b..dcd63e0a 100644 --- a/server/controllers/api/logs_viewer.py +++ b/server/controllers/api/logs_viewer.py @@ -12,7 +12,7 @@ import json import logging -from digi_server.log_buffer import get_client_buffer, get_server_buffer +from utils.log_buffer import get_client_buffer, get_server_buffer from utils.web.base_controller import BaseAPIController from utils.web.route import ApiRoute, ApiVersion from utils.web.web_decorators import require_admin diff --git a/server/digi_server/logger.py b/server/digi_server/logger.py index a5bd38a1..3c076c5c 100644 --- a/server/digi_server/logger.py +++ b/server/digi_server/logger.py @@ -4,7 +4,7 @@ from tornado.log import LogFormatter -from digi_server.log_buffer import ( +from utils.log_buffer import ( LogBufferHandler, get_client_buffer, get_server_buffer, diff --git a/server/test/controllers/api/test_logs_viewer.py b/server/test/controllers/api/test_logs_viewer.py index bf68508a..05586ffb 100644 --- a/server/test/controllers/api/test_logs_viewer.py +++ b/server/test/controllers/api/test_logs_viewer.py @@ -4,8 +4,8 @@ from tornado import escape -from digi_server.log_buffer import get_client_buffer, get_server_buffer from test.conftest import DigiScriptTestCase +from utils.log_buffer import get_client_buffer, get_server_buffer def _make_record(msg, level=logging.INFO, name="test", **extra_attrs): diff --git a/server/test/digi_server/test_log_buffer.py b/server/test/digi_server/test_log_buffer.py index 08c94a23..2bebd732 100644 --- a/server/test/digi_server/test_log_buffer.py +++ b/server/test/digi_server/test_log_buffer.py @@ -2,7 +2,7 @@ import unittest from datetime import datetime -from digi_server.log_buffer import ( +from utils.log_buffer import ( LogBufferHandler, get_client_buffer, get_server_buffer, diff --git a/server/digi_server/log_buffer.py b/server/utils/log_buffer.py similarity index 100% rename from server/digi_server/log_buffer.py rename to server/utils/log_buffer.py From 7d15f904c4d67cb1fefdd71415760c7e7de126b0 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Mon, 23 Feb 2026 21:43:35 +0000 Subject: [PATCH 40/43] Organise system settings into categories (#934) --- client/src/main.js | 3 + client/src/store/modules/system.js | 16 ++ .../vue_components/config/ConfigSettings.vue | 190 ++++++++++++------ server/controllers/api/settings.py | 7 + server/digi_server/settings.py | 42 +++- server/main.py | 4 + 6 files changed, 195 insertions(+), 67 deletions(-) diff --git a/client/src/main.js b/client/src/main.js index c3e1d153..091e524d 100644 --- a/client/src/main.js +++ b/client/src/main.js @@ -13,6 +13,7 @@ import router from './router'; import setupHttpInterceptor from './js/http-interceptor'; import { getWebSocketURL, isElectron } from '@/js/platform'; import { initRemoteLogging } from '@/js/logger'; +import log from 'loglevel'; import './assets/styles/dark.scss'; import 'vue-toast-notification/dist/theme-sugar.css'; @@ -22,6 +23,8 @@ import 'splitpanes/dist/splitpanes.css'; setupHttpInterceptor(); initRemoteLogging(); +log.info(`Running in ${import.meta.env.MODE} mode.`); + Vue.use(BootstrapVue); Vue.use(IconsPlugin); Vue.component('MultiSelect', Multiselect); diff --git a/client/src/store/modules/system.js b/client/src/store/modules/system.js index b0c7bec6..2cb19096 100644 --- a/client/src/store/modules/system.js +++ b/client/src/store/modules/system.js @@ -9,6 +9,7 @@ export default { availableShows: [], rawSettings: {}, rbacRoles: [], + settingsCategories: {}, }, mutations: { UPDATE_SETTINGS(state, settings) { @@ -23,6 +24,9 @@ export default { UPDATE_RBAC_ROLES(state, rbac) { state.rbacRoles = rbac; }, + UPDATE_SETTINGS_CATEGORIES(state, categories) { + state.settingsCategories = categories; + }, }, actions: { async GET_AVAILABLE_SHOWS(context) { @@ -88,6 +92,15 @@ export default { log.error('Unable to fetch RBAC roles'); } }, + async GET_SETTINGS_CATEGORIES(context) { + const response = await fetch(makeURL('/api/v1/settings/categories')); + if (response.ok) { + const categories = await response.json(); + await context.commit('UPDATE_SETTINGS_CATEGORIES', categories.categories); + } else { + log.error('Unable to fetch settings categories'); + } + }, }, getters: { AVAILABLE_SHOWS(state) { @@ -102,5 +115,8 @@ export default { RBAC_ROLES(state) { return state.rbacRoles; }, + SETTINGS_CATEGORIES(state) { + return state.settingsCategories; + }, }, }; diff --git a/client/src/vue_components/config/ConfigSettings.vue b/client/src/vue_components/config/ConfigSettings.vue index fbf7f9af..b8b917e7 100644 --- a/client/src/vue_components/config/ConfigSettings.vue +++ b/client/src/vue_components/config/ConfigSettings.vue @@ -9,62 +9,96 @@ @reset.stop.prevent="resetForm(true)" >
    - + +
    + + {{ category }} + + {{ Object.keys(settings).length - dirtySettingsByCategory[category] }} + + + {{ dirtySettingsByCategory[category] }} + + + + +
    +
    + + + + + + + + + + Unknown setting type {{ setting.type }} for setting {{ key }}. + + + + + + - - - - - - - Unknown setting type {{ setting.type }} for setting {{ key }}. - -
    - Reset Submit @@ -80,7 +114,7 @@ diff --git a/server/controllers/api/settings.py b/server/controllers/api/settings.py index 4a0fefc3..4a203eeb 100644 --- a/server/controllers/api/settings.py +++ b/server/controllers/api/settings.py @@ -41,6 +41,13 @@ async def patch(self): self.write({"message": "Settings updated"}) +@ApiRoute("settings/categories", ApiVersion.V1) +class SettingsCategoriesController(BaseAPIController): + @allow_when_password_required + async def get(self): + await self.finish({"categories": self.application.digi_settings.categories}) + + @ApiRoute("settings/raw", ApiVersion.V1) class RawSettingsController(BaseAPIController): async def get(self): diff --git a/server/digi_server/settings.py b/server/digi_server/settings.py index 2f582c50..8d1274f8 100644 --- a/server/digi_server/settings.py +++ b/server/digi_server/settings.py @@ -4,7 +4,7 @@ import os import tomllib from pathlib import Path -from typing import TYPE_CHECKING, Dict, Optional +from typing import TYPE_CHECKING, Dict, List, Optional from tornado.locks import Lock @@ -167,8 +167,17 @@ def __init__(self, application: DigiScriptServer, settings_path=None): ) os.makedirs(os.path.dirname(self.settings_path)) + self.categories: Dict[str : List[str]] = {"General": []} self.settings: Dict[str, SettingsObject] = {} + self.init_settings() + self._load(spawn_callbacks=False) + self._file_watcher = IOLoopFileWatcher( + self.settings_path, self.auto_reload_changes, 100 + ) + self._file_watcher.add_error_callback(self.file_deleted) + self._file_watcher.watch() + def init_settings(self): db_default = f"sqlite:///{os.path.join(os.path.dirname(__file__), '../conf/digiscript.sqlite')}" self.define( "has_admin_user", @@ -205,6 +214,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None): self._application.regen_logging, display_name="Log Level", choice_options=get_level_names_by_order(), + category="Logging", ) self.define( "log_path", @@ -213,6 +223,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None): True, self._application.regen_logging, display_name="Application Log Path", + category="Logging", ) self.define( "max_log_mb", @@ -221,6 +232,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None): True, self._application.regen_logging, display_name="Max Log Size (MB)", + category="Logging", ) self.define( "log_backups", @@ -229,6 +241,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None): True, self._application.regen_logging, display_name="Log Backups", + category="Logging", ) self.define( "log_redaction", @@ -237,6 +250,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None): True, display_name="Enable Log Redaction", help_text="When enabled, potentially sensitive information will be redacted from logs.", + category="Logging", ) self.define( "db_log_enabled", @@ -245,6 +259,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None): True, self._application.regen_logging, display_name="Enable Database Log", + category="DB Logging", ) self.define( "db_log_path", @@ -253,6 +268,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None): True, self._application.regen_logging, display_name="Database Log Path", + category="DB Logging", ) self.define( "db_max_log_mb", @@ -261,6 +277,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None): True, self._application.regen_logging, display_name="Max Database Log Size (MB)", + category="DB Logging", ) self.define( "db_log_backups", @@ -269,6 +286,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None): True, self._application.regen_logging, display_name="Database Log Backups", + category="DB Logging", ) self.define( "compiled_script_path", @@ -294,6 +312,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None): True, display_name="Enable Client Log Forwarding", help_text="When enabled, client browsers will forward their logs to the server.", + category="Client Logging", ) self.define( "client_log_level", @@ -304,6 +323,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None): display_name="Client Log Level", help_text="Minimum log level that clients will forward to the server.", choice_options=["TRACE", "DEBUG", "INFO", "WARN", "ERROR"], + category="Client Logging", ) self.define( "client_log_path", @@ -313,6 +333,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None): self._application.regen_logging, display_name="Client Log Path", help_text="Path to the log file for client-side log messages.", + category="Client Logging", ) self.define( "client_max_log_mb", @@ -322,6 +343,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None): self._application.regen_logging, display_name="Max Client Log Size (MB)", help_text="Maximum size in MB of the client log file before it is rotated.", + category="Client Logging", ) self.define( "client_log_backups", @@ -331,6 +353,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None): self._application.regen_logging, display_name="Client Log Backups", help_text="Number of rotated client log file backups to retain.", + category="Client Logging", ) self.define( "log_buffer_size", @@ -341,16 +364,9 @@ def __init__(self, application: DigiScriptServer, settings_path=None): display_name="Log Buffer Size", help_text="Number of recent log entries to keep in memory for the log viewer. " "Larger values use more memory. Changes take effect after restart.", + category="Client Logging", ) - self._load(spawn_callbacks=False) - - self._file_watcher = IOLoopFileWatcher( - self.settings_path, self.auto_reload_changes, 100 - ) - self._file_watcher.add_error_callback(self.file_deleted) - self._file_watcher.watch() - def define( self, key, @@ -363,7 +379,11 @@ def define( help_text: str = "", hide_from_ui: bool = False, choice_options: Optional[list] = None, + category: str = "General", ): + if key in self.settings: + raise KeyError(f"Setting {key} is already defined") + self.settings[key] = SettingsObject( key, val_type, @@ -376,6 +396,10 @@ def define( hide_from_ui, choice_options, ) + if category not in self.categories: + self.categories[category] = [key] + else: + self.categories[category].append(key) def file_deleted(self): get_logger().info("Settings file deleted; recreating from in memory settings") diff --git a/server/main.py b/server/main.py index 3da787f7..80e8994d 100755 --- a/server/main.py +++ b/server/main.py @@ -81,6 +81,8 @@ def patched_define( display_name="", help_text="", hide_from_ui=False, + choice_options=None, + category="General", ): # For database path, adjust to a writable location if needed if key == "db_path" and default and isinstance(default, str): @@ -106,6 +108,8 @@ def patched_define( display_name, help_text, hide_from_ui, + choice_options, + category, ) # Apply the patch From 85ae7d928c6dd7fe2029faddf5d15358c6451f3b Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Fri, 27 Feb 2026 18:02:50 +0100 Subject: [PATCH 41/43] [npm] Update electron packages --- electron/package-lock.json | 41 +++++++++++--------------------------- electron/package.json | 2 +- 2 files changed, 13 insertions(+), 30 deletions(-) diff --git a/electron/package-lock.json b/electron/package-lock.json index 2c53e314..878a11a6 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -19,7 +19,7 @@ "@electron-forge/maker-squirrel": "^7.11.1", "@electron-forge/maker-zip": "^7.11.1", "@eslint/js": "^9.39.2", - "electron": "^40.4.0", + "electron": "^40.6.1", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.5", @@ -1169,6 +1169,7 @@ "integrity": "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@inquirer/checkbox": "^3.0.1", "@inquirer/confirm": "^4.0.1", @@ -1800,6 +1801,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1875,6 +1877,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -2116,6 +2119,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2865,9 +2869,9 @@ "license": "MIT" }, "node_modules/electron": { - "version": "40.4.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-40.4.0.tgz", - "integrity": "sha512-31l4V7Ys4oUuXyaN/cCNnyBdDXN9RwOVOG+JhiHCf4zx5tZkHd43PKGY6KLEWpeYCxaphsuGSEjagJLfPqKj8g==", + "version": "40.6.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-40.6.1.tgz", + "integrity": "sha512-u9YfoixttdauciHV9Ut9Zf3YipJoU093kR1GSYTTXTAXqhiXI0G1A0NnL/f0O2m2UULCXaXMf2W71PloR6V9pQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3474,31 +3478,6 @@ "dev": true, "license": "MIT" }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -3616,6 +3595,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3676,6 +3656,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -6226,6 +6207,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -7644,6 +7626,7 @@ "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", diff --git a/electron/package.json b/electron/package.json index 959549f5..468664e7 100644 --- a/electron/package.json +++ b/electron/package.json @@ -29,7 +29,7 @@ "bonjour-service": "^1.3.0" }, "devDependencies": { - "electron": "^40.4.0", + "electron": "^40.6.1", "@electron-forge/cli": "^7.11.1", "@electron-forge/maker-squirrel": "^7.11.1", "@electron-forge/maker-zip": "^7.11.1", From db36a938cf8754949ab31ef674368255be3bb107 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Fri, 27 Feb 2026 23:21:30 +0100 Subject: [PATCH 42/43] [npm] Fix missing deps --- electron/package-lock.json | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/electron/package-lock.json b/electron/package-lock.json index 878a11a6..02ff4e4f 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1169,7 +1169,6 @@ "integrity": "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^3.0.1", "@inquirer/confirm": "^4.0.1", @@ -1801,7 +1800,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1877,7 +1875,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -2119,7 +2116,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3478,6 +3474,31 @@ "dev": true, "license": "MIT" }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -3595,7 +3616,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3656,7 +3676,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -6207,7 +6226,6 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -7626,7 +7644,6 @@ "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", From d5dd477106da49bf965b12323003187513647150 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 22:26:20 +0000 Subject: [PATCH 43/43] Bump ruff from 0.15.2 to 0.15.4 in /server (#937) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.2 to 0.15.4. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.15.2...0.15.4) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.15.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tim Bradgate --- server/test_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/test_requirements.txt b/server/test_requirements.txt index 6b5ae44f..4c4c4e4b 100644 --- a/server/test_requirements.txt +++ b/server/test_requirements.txt @@ -1,3 +1,3 @@ pytest<9.1 pytest-asyncio>=1.3.0 -ruff==0.15.2 \ No newline at end of file +ruff==0.15.4 \ No newline at end of file

    IEx5rXzdl)Hl-*}=Mo8##K|tAacwG6=#I0^RBb=IvILmj}*-NhD?Y}E%IRl8&|nXD08PcETL9jPfLf5}Z38$ zHsOs5?h!=OE#`V(>~{i^Enc7qNLk;pZ0UI^N1-CEzBii3o&2T#QS0ZnW&>#B)?grg z(oXw+D);s&*~X;%x9F{r26e2`NQWfrM_Di#xg0B-}5dQ+$gqHeSYdW z%PeVOVczv>v3U*6u~l>EI4&X*j8j(@Td1pttKA<#b?- zyby7nCY!BM6hq{2c*#}e!<(?ID*iI%C|L=;(6zW#Bg+%I*Y34R$$otK8|^o+#U>_6 zY4#IIlT~hOyZ)ol9DW&Wea1jpULMPqKG(E}qMLKH}t3a7TX&^upH;BMM(b?|4et11wNu6}r@*}?P zz=|8OJFI9kM-aY#UWyL00Z*(~X#U!_!A(y7IHMBCT0`1+p}@6+)1m><|i4xe8##LjPXf?0^1zRXfwevj=pg`jQ_{ozLpT zHpNYj?jZT4&dt__G8LvA4gp=?qQB7WNnO%Z63FO61w}CVkix(RvqX4sfQ-dIA_}CT zrZzL}R8HQ6ZX0`M5YIpl7l@^Da-MAs=ayjwmsd;Tyl?Z1+0P-hXA)M}lb zDq^yj1C`i4I$lXxIur^zS9#IoxmXmw0X-qv@&6Ljmi`l=no*Hf)DP8ZyWvyQS-#~n zyDDSJXER)o`;Mo3P=cs;VHLa=7et|VzDirP24VApQz*IFo`(2^yTyt>Z{h9DJr4ad zcAoZVQcZS+I4X>A-Dad-#7N1tJaw40;CY^t&uBoR&z|G)uRKwM1lNHt3#6FVuvD%h zKp!MrukKK#6<~4vn((;!!w%OKFS`1`}PQ#JrB1tfZU@l$> z+3};Hmb16Xe4>k8Dh1w@se6kRkuN=ty=Qs6U9>!O3J_1>B9(Pw6IozioHiF1{loJ7 zS};20vzvT^f{JTLJ*-|_(LdRLhD`%H&nVO=!D1NDH?!6mnYeMYEDEt0x zLTX0&=+4>xQ)LIDKqiuxK6?isru{YWfi!?EztL!6mLP#>RY8$}?8U&(|Xt_%HMh>-l&xl0&IEs<53wG2aYzk8_v zo$2fAv=E+#vlifw%~6n0LlN!bM4n%K4)$n9%yHIvWtu6RRsKOU*IK6M3i7qK8~d55)5p38rKS8uqsHvFlR}!&!l;% z%NA?K`+_RBF+7wKdojP9lj?p$#0?pr;Vk*UrTPg>4M!ZEVX_E21)wW?*MGKsg0_oG!? zXe{eYmGuSnoN5I0_h&yU59mIIiMwnI?-wC1|3Zj*LL@YFrY>hxj>@u~d0*UyNg8Y@M&KTF8DL4EVss zV7Kr1$KLBoj5$+BWX(Bw8UzdJ3weRhu5%Zl)jsv z4AUyH(+xEgU*A{1xEMWMhD!B^%AYT~UF`Hj4>nOkfGmY|1(+f4iWM9=z3_b&69WhC zcBmv8Yw2?kgSRW2+VkFOjFz$guX@7 zV)2wZq|fs#+L56uBQrsoLp)2*x5voxNZP{mR$FZtxwP#{F>R#@>YL#U2i-P;Dz zYzwJvwXnh8d}jv%P0e?DJU98qbF6GOw;_@!BlXcE=`PGTVq?+JxM+?54c+;5O4u9_ zL=k+qwb*YrYQg_nj0a6e5 ztK)b=)ANDTsF&J=hl}pnL4)f~G==%nD?Zv_xyI`Cf@2jQQpKq;@%?;7+ejbXio6EBQ z6Z3$fUTLzDkzUAWE4fNVY@vrlKAiT&VzNI02m9gF}^v~)h~xLk$SH} z9#R^(9FI9KG+0|_Cp&AIOfQDhi7{_Vt|X3&QKzY|^tniR{2G9?w@)TT^=De^t_#|C zcUNke=~vj8qa=gD>C+DG^QOkewjzI!5WCBn4rWP|m6k3w$^sjSWiscUbCh=zbS|28 zHgfil*tKnThln*74rT8mQIpq5EQ)c^evrfHc0rIaRYLmOYjp~HjO{EvWXZ#my-3Bj ziK0}Iq=H)AT|w%I`@!7_=a;ghU(?xi{bmJ&)uU7DhDo(vNN3xG;R}0KzfnGyUb0ol zd+6A`j-H5I7Trlx2p8g>D0kXOuUg^sw1(vO@pCDt&l8Yzb`2)OHZ^Dqk64Il0pXp1 zpOM&?oHoiBh;FUCvw6qZSKk|DJ1Pu#rCE!&wpV^xw-*}O?!*fPqkRkkyWHLrWeRfAbBNN= z`40MK`mUHKe4Khyn3R2-NU3bU)42(*K&jKsD=GMfV_^P-xj8*@sUqF*cuC8QA%yv| zu-(7gDb1yOI9&{-a*+YBjN(eR=T$OnmlFszbsZuBzER$a6<3q+LXS5<VctlY_!T5!p61ZY&WrdR8K0jPYFeWyZ=a4Eso-EwD zvDnD()xw#n0k4s>9@}I;aiViCVPJ<{Faa=c&C%7T4G<|-iZmXxO3(j=7 zF7;AP6Uk5J?r3E-ruJI}g6yq`rJ;ZBG!QPw{mg1`-_*WW#W3~&bx$EK?Ql;YU(Uz*bDO?h<3CT&A zsD#Sb5T3WM3ibPpO_9IH3TGrFRnm@Uib{k9zs**69wQ6sBqk$~5HU6wTw;?R%i}|; z*X9;@dN=D)fL9uvzhJ+OFseJIZtCedF7aQ;<}}tS_qCC>q|~pyQRFOUl}k!(nodA&8w$p2{Iwt_VL{Q1Ss<`7gO?BHvJ~eM9r4ZM-aEh< z^`C~5z6nr>x8gYVTMD(FY99_A$a$I$=8pyV1GK=eluEtA$KjWB{qcJxqw#VcGh%)8 z8A}E?ukmLCYWl`TNhYQYKybUJnO7+?S!p0vW7KfMioU4SQP-X*_Dc>5#241?mRf9? zP+ROy9L{*^IRG!>{PunEjeR~bcZ?T?0_3;XJGwqnXz%sbrFklKE-#+%Hn9jNP<3&@h2;kC@DusPnLr6WNc8f4g>VbkY-c8 zV&j~}>9Aew!cMK(V@5B*Hq_WZ-#)yQKd zH04bgiazG7!w~E?CL+ronJ?3rccCz*tZGj5yZljoUT=GTa_8y|_AeyU_v6xrWNpb> zE2*{Z?(mgB%91=2)^G@JkBuEc@N8LIZEQf0O*Zg z_{Nb>77rY@wO<_KP1;*b815)SOL38|fZidbp?Q3L6~Lt{Cwe4bb9$gt>X9~;`uup& zeuD1>Uni}E7ConKwbKeyzP_=14nVR6TsMe``WlQyyaiZOQ>k~P*nm;znTjvqC&ky{ z5O1C`>eWIv;KH`{xeLo~qU{dCCPzVwEuC*Aybnosw9Otr_;%ZNBtN_z%jgpgRN#?c z$k*P_Wm5-pHtkOHBxV~Bn4!_ z?a`m(b#*J@k$L9J*6|{`askURVcc!yx)TB_Kj*y{4#V0D_6f?(E# z{H$CtgAzVy*TmO~y#;~C6q!~ZvX{yp7R6OqwdXVES#llNk704Oqy4&S4f(^yfp@^! z17-JI%KxgcUPkoe_d7x;rQ#lyp?~6xNr3n!=ajZ3Z=HP#3pPK6om^9`@+KKH$}&d% zJ&JDHP`q)wUa~ZTo~L{DJ)J~2Rookffb%pZslo{D!69?z!Iv_XZq$?6>qmZF*I}Dj zes@DJ#$jy^=sNTV-@TFAIRWc*&jg zp2DQT`|DD_@r6yiRLiz##CBob7El>?qS*L+(y_5|k3`UcGyY^SaQ8Q{`wS}6cGS$f z->@MyVRuVv3hb_%&@fcE>7)-`aNMe0X~ZTa$pqr)M1jh;M|Wt*xOmwbRd;_kOOtVl zvHL%pDPST~BZC`Kc8CU?4-KoWp?!=ZH`>fEldtNFSKlVs^NsH_L|Iiq?qyjB4f@m`f8` zSvPi7wjP0zZ9~8rZ44^no+o&) z1mxrB0vOSkeh~(Kh=g(m$L-RP5eVH34N88szib|gpT-oNP`V2j7SSn&#(v=E;*0#M zI82~XMoX6!+W%+>f!dPCrMkR7MFx~7PkE0`M@M+>pS2Z-NF~|Emo}NmhUvIBMq7!S zcB4Bl7V0-G?{7FCZ)eQZd>C-eeq?Q<_=qDlUcIUC9YlD_54M*Gg?T)Y10#*{G&Gfd z&?oO4ezW>+q&WU6B#^qPanFFs=2u7bR*ln{X7t`<1^XJxcnZvFw7=U6@^xAH867y; zPNylBKxJg0OJVkpvph5W!;HF(m*M=+OJyP!I#Q%}`dbR`2ON^6AVYT-SQPbvD9q%7 z!(>)LI-+r6LJE)RbMhEv)@AVCPfKa=s^VhzIx5MAa~hUgqq6#d5~!h&A#=kL`4>*5ZcA zB6=^pMsV)KU`=w}_Z1^VAp)?vaoK>9A|s#B#B%GLO?}UfPELh7bOX`##rs8# zI6Qa#Q7Pg2XodBpARkIOFcmu29wB*FL`X9twR4mKI@<*SF@en^)9{#BWbnb3)Xzk? z7HCC5lbKwkIL&zw+P_e%_FTz?>Mp|cM5+uQ9(!y<+ zGjX>@rVf#_^DB9?80C@B;O9J$eS)$s@TUZ)#qeZ_@YMFc3R~UT)ZnBEA zquOh$$zZhXc&+BCe#|>h?0dM|DGZXt+b6b)f>Xp_s>NJbhCQ$h zdxOL7Rhc$6?#Ebt5m`t8bx685av;T)L9D+D;P?s$Q$g4~}2xR5LW+VO$VH3u%^q87N^Y@2spqjw>ZfOYRMe$2bG;j=m zNKp|Y!kPg}M((qo z@{BFjA1)4?YL-Djfan=)C|BLGCO+%9eZSwab`DRf@RU{D2|vrh!A{DD87PT1e$+d| zOC5G`FynpUH7g;Rs^Ztvi(DVibC;6~NIHfZ`phlU6xxbfa8jQiph1!szobiDy}Y~t z|Kkh3vg>~M=m@FZu?%9?llqG_B>r$6sN$&`6CKUx(-tXzzsl8Ow(ocu*mEEI&A2nl z!p*05>hw18r$z`tM+eZ9U(cAc@99CTBL+Cp0hbK=9uosQ4<$9i3sIT4K1bo(wP6aWy?*P0gy7Kmaz{rpA+l+*nCaSuNu@DyR+K zgq!Y}oVl-~CSuZCA6684hMbSJ+*)K?JpHb&lmxyfHnCc4B+K|$?o+>;rE0kEctdxB zm4ua*2Hu?u!kI1#yMM<5%CzE2I?%OYR(7pP6zVK94JQr$yGks6{E%yER_s?v20#7Y zV#Z1`#)MoP&$Ku5F2z(3omlbA%Mu|6gqNioU2vO*St5XR+L<^hUs$3`5YX{m$_aOi zqPS{Cp&!~A1rAjp2lOivYeQ`={c>n807uvTWq-lJ4JTR)z&x3djboyzA*sW=FdK%Z zmom~RfMnh)6n^+FMZiX|TaQ0E8(?k&grE^DEt6EAoyRG+^R}p4V`iP>#6HiGA6AX& zCXpNv+Li89PE&O{KRJL~!-m;lLx|$%K*jRj1oR(GP2-Z*));`O5Y%o0U@G#9@+Jo# z1Eq)Ad3ZR0=m6-D{MIclQSfF`a&nn*D-f_9<>%+e{%|$)^8>ye+5Haa_?*az#xI=< z*0X7*zP{5PQBf9RKiIEY{!AQ;`NiBXB3%;46gZ9BIhpKa*LzX*k`FJ;Wx! zHd9bgP?y-b?gvSd7(_Y#x!&`!f0I^KxBA883L0*Qv(JAB&%D!~wi>(^Y>3WeOE9K- z!be)k2ve0n@EIIf*jPKXvu)Axw@RY6_|6C!S2^FgFJ*%`xq?RmH*Wr5mv_qKFsxb| zrcjz0?H(x)$27mmG4a*0_iuod)vML6Mc&BD<(5@zWi+XPtQSZ`I{om{so}_3w!)0t z$Ym|Z>^Za7p4oJzTDQS&6%;3VHc)F!c#0di`YVjrXVv?7zWuoZ@}JkB-Hqhq7s;Pw zb8CI^MJuQIzAQ>ctLi$6Kks3zjq%{40gYtX12vH>s=Mq@Kx)f^Do<|juy4v#6;s7X z%W_F;esAN5ig-M$^7H=f{BJ&TN^0hH3d*mZ8FOHTq+dzEM?^Z`$8=}X6vSadoAi!{ zcj+t;hR4|8!?G3G2N5M$Xc|X{pI!>t?H@BYh@($s_ZqLHIB`FLGFoYk(}sZEf*~F# zW?IenLis}4Y@9O9(J(lsXzG|**U$N~T8`%iP;8`h-ZsOfqF51N&Uz)%W1=020YumD zknFG4qxPluy^Yl^`*{Hd7Eh0MzFUo2#YoOXao*dzTvhfHZRshMlTQdloetKct^O(yuiu8S}QWwOM2VHi-s*s43mN zt%P>dt~L0k0&Z)y$1ygUAa|P)>7ri^7zR}p6)F}2K6R^tW#9I^m}<#=26F(Mz(GaR z*r@5nV&3)v|7B95y0t)Cuy5(M7gSchLo&8y5)LwgpZ7 z-Iqj+mJbkNN6QZP;*5>kU!eItc|h}WJ8daRaSpi`QQsVWm4;y=^Lw!iXHwpWbWo7N zqYHeJ>!_u9)U8kVNm~M*n-S|UDyWf!*e9dki#0WA5F9^pv-4WzSh$L}bOcqhY3#>1@ZVUS+Snb9^k! z*>LLU`P+oW1^q^NerlaXJ6}KXFb6sZey%=AA_V)1RTsM6wmePq>BD#5CpG#>>l-xM zhXX?vEMp!9F1QNvD4$%B+s3w%EhY&JV-wk{cFPgt1vFdw%F$TBWky6Wei&`QddgDP zv^?|!K2l~Y$V%nl6P;PUQ@dtj{}(=Y6H5k`X@A<)I78@cxlYkjus^`@@4(fV*xd*2 zpVsEbC4_ttd!J_`CgsVA#l-Y%@+f0dk1)UU_0}Zx5(zoFzy`RvDZgk$zf^p(wo%=j zf7JP*4;S>eTIz^ozTKAa$~ii-e_tOjba24ozMEUBCsX4ytJ3s* zd*EP@Mcs#jNHTYCUW$v}S-HVc3Bo_At|OecD+NvXU+sN&RFm1dek>z1I*N{k(Gc(q zV?hQGrAdn;3Im833kabI2uL?{2oTgk6a+?)-bN8nks^UWz$8lV8ESw)f>eRf2?5f6 zn=?{o?po(}*1hN6b?02NmjB4Nzwg`ce#`Sd@4NTsxG!8KXDq`01X=Yrb{QrN(h*aR zlF|-FJQI6Wh3bx$=RFdyT*MW{W+4~NB~Uugj|&Y3qvDS`c2*lFN>o&Q$T!xqx0N{J zz!1+@7%i*Bqn@@tx5H&b=p!|$hopB&?d$uVZJ$_)I@PLnpw6~n&t`cZTdd@Q7{);_ zxZ#>ipSRq<5@(>G#Y%Q?x{He~&a zE}ewDF-r5f(;7eIXQVOWnJ;NeBHCKdIuBc?6#o*wchYRz?=bFW&XKd-aE1>;3!?Ae8V8Lw(KWP$6`sE@k_o@*Dqn7q z7S}CDy8$bFUE(VvwJ+i77HjkX}}N9@USI*V_=V%osPHEZs19$^vGX=#@&o}4$E)Uq@$rXPl0Xo3IyxaLk_Kxfff;Z%|*#C@Um!zuejvXNbND~_YJXmGV12Ww=196 zN@`P^?b1-J7~kOby*RZCs;>=NeB%8G4SlC6zPb6+1A*;Av8uC`T!bon2D;M%b<(jX zpH^%=Hr6wcw|NCo&tKVGZgLcMgN|VtEKTz4c)z9w2PI!uVlav<&!Z|w0`G^J@_R_M z{8{@1Z(H01X|^kCTVO(a&`zd?rOZ@_^VYVYI3QbRLs497bOX$HP9Uvc%2D~FuIlFV z0cL_)!ow{Z4f+pVY`bHh=S-XSS_>B1_O;tB9O`c#64%Q9w#}~LsB=rU6#df<=&&{a zj#W@F>KRS3_+xz5(w#E*c=UGL?0x%@6kA%F4B4W8qG6x3W4%ypUB==S%O(0w%e;_$ zpAYTo>7AkR6{bj|OnDrjnlbNn5d+GHh1;v4+5+JJ^@+yE8I$|{^8>gv_V z)z(#R4YhTZDrq*JJk}mS61UvF|0P)r6|7#G*B$VumVV z!keb-dn?}LE{65Fl{Hpo#vz>-dSp@1+J!@S)7IFnZ$&b)RHV92rdaeV)l@pXZK9>w z#)tOw#Wxg+R6%nhcbR0!Al%;OD}nhhbEQ2J1}*MlK`Uh3lU z7$^4C@&<`=bx%A0{ZL-lg1@{!L7iaJt@zZw+pX+aFmC=?>&jUVjDoEXOLUB2!d{;q zbk#1FWiNOoN*5%#U6^d($BvUjlfIG5(0Rkk>RHTeLmOz`wW}L;n*3C|7#eg?PDTnB z)R^_<`%J_;8C(d?D{V6R5%srZ>uo|i+kz5A9Zj*J@JZdNnQ--_6H^K4G$7wCTT=ziELEg2@)G=-9aV4HSC;CEx@4w&S6E!HC_mTVupYzstuaE30F?6rliMfBzb2-N<6U59_ zQt`dVSG&z#;Ucqd!pt$iEFfG@p<^A|5DS74AikDUssN7F`ZDjmyWFP%1F({-f6s5mYw}s`6kNZRyeR zV1Ws~hxz z^KiN>n{&&Zr;eh0QZySS44SQ-rtBrYsZT;bhuJ;#w)x3$LDJHE_oqjAR7;*!*)t=n z-Y3wdNiIE~gvLe!2;*tS(i}f#W~lUWru1n)HVv2g7#aTR4?IeqnjbuV)4rGBpMH_SEDDM%dybO@isR-TP{d zTnxCm8m_=soMP8~^Y+VsFr?h?veg8kAkFo;3uf)UB|#9BXshRN%-ULYByC;m9X@+< z)7ty*z6ds5to*j_T~}!P@TVcoZR)-&T)@fz$bT6Ap7x13Qnm8r0K%WA5PV`sMp5W?b8K0iEwkn&G1>blqcv+!0Y@?Te-f9X@Yxh{dF zFD!Rl4byuN8mg?(PRV&GiB2z`i|d-PX<4tEIp;Q)_hh;ac*BOdrrq+4BWRFlyv;Tc z5aIdr$)xCAb89a)_4+&TU4w{hu97q-vNS(3Z`_B~Ji5d6wazerVWB%5@hAbIa22@1 zXaH_6{hG?$V|O$DO~3ISYkB=UpD%ZNhH${=kkDgQ{Z|Z(4l>v^!`p0nW+KX5C&&4V z{HNBY-Ka)(q`75~3D2O%Y>|6hcSU<45U;4+hbMec#18N2l!~=VTf0PGhd!*CPyq ziV2P^{g6|``vLB%cP)qbc*j0LRqWVK48ost4ERHzy41i|IR`x|=us*7!r>)SC+9|l zGn}hHy;P(k<>AmB)vjMZNf@NRmR2cP?D*KP)JQ1ysO<9Xadzg5BPA`PH_e;tfx>B@mVTtHId9B=`==*Wz*3E%AhM;Z z9t{J*_%e9NOgEbC#AW%AN1ur;-VdAbDbrLkWh=K;gd3z_@(QqS1B>`30ez5nu|fq4 zOy^YY&=rD%l6u@n0oE^B#x}X?26W`)1zOXG)zi4DR%J8vw{p-2@chc5ISF@GeqkC!F21& zO#;S&wrZ>IG;^wddOQ3fFc5^mi>SLklkqR?=k#NM^$dyWIwf6+bM<_h4r04l4$rQt zbFx7%w#jS!)XH5TKUZrqlmKBc)}s@-XRnK8#(DI4Cr0b>;Qn*eYC3xo_E^~O@{wX` z>dPMz+t76+w?x+67xpAvWKMl_ez%$b(gZKXuYl-(VfDoz%0C7ag+vY0C?2JQX=07z zq+$D!_e-A!AN6NVHVVnpquQaFcbQwlY5N=v97htLjen?eYDmUziE|4( z0pk|;earzvi|?BGp8nklgLe*qE!)r*(t`EK%h~?Rh9DSURo!=?+QgfyaFbs zK)s5TfoqSUUb*%Xozn-2w}+iuNKR8v67tff4-IQ+7A7r3m(E>PYV_B0pJt3k99xSZ z9ogSo#Jpy`8D)&)!6X58`R{c7pbze{AvzN=6kS(TKEQz|++($O&gqU}T1 zXayp1UwVo#;i9e8$2ZAP_!CucF^xnYi**87b`bzZ764>%JS>P?nGvFyT$uC(&po;m zJ!=-eFpiPF2u~}>zzqdDB#FW$56XLBKP++uB}H7iqJ<6?@sXXy{fTtoY#qF1M-Bls zO*v*_`Cz_0@XGWrEegKMW(+;Jf zcx76{FF!i7Thk-OsUU`+DNfP2sKcGezKOoJznm8}rW{VpTZVklN1m1_b ziUK1GTh+FknlpP!ek9lo7Uyua01@JrI2{J?q>T>2Q+GTtBeQNu1C{tV_A(HkJ!sj- zvRJ&Slol6$<<%AJGxpmgnogQgfCo|ns1reS#8R2O z-jre~_AKYes>fbcHYax2v_-4!jZvDt8det?9}CNpOrXx*$8_Ieenf^+mXBHr!hK<1a0JzHe2AZK5t<*D1%IO?^(0?E?PO4Yl29W#9u(dRsT2IT7by z`&(x8Y0wiZ>@~n1tit4VdPfaUnDLWHqrN?&!y38{`+x3Ct0i|+J*hUp%JN7@(!v@p zy#f>vi)r6=$;*}apyji9u_bvDgAaJ)tCvV3^*8({moV)&D zC!6BWD|8h*_I>Iu2e+T=QnFatkK&(w9Mv-`3XN>O8{||S4VV)D?)-fHSCW25v%Yq4 zzrFZM8GA(PsY`$Q5tta1jI~1;!5*(I>hPDlSwAoP^?JgO8M4J?a%3BweaFYgvgo(K$x$DiD5(C ze$BwS@Gd3otpb@HPToHAKstA9oo%xLc1*=Z()E>=+&eXtI!fkUo%JNPD66EMWR{y7 z_`WOXaphJ48=mcZR#R1wve*cs7K}3}xOR_)DDCe&NJXuNDl_kr4t9EUTTxZqW}+6i z?3z>@>*-{!zw(>I6CSpBII^(eom-GtgH#uxPg zMbtFz7yu%B99hiTo%nMju{*GE0a3in=9QHWAS6Igdf)mkuse&rKE-KORXc`?i+JdF zt34mT=^WO))UH{sWqjO9=n+o* z(t~gbKs15)6w!#F9(w8)Dt__IGY~ECZ?9rcugT)enD7pBG|b;*c#Kc(O;upgmL?NK>-Kl)@f&dc(%~WF>g>@To#jN{ zyHQmg9kC9c7S01iGy{kNfJ4{GM0Qi3$f<=^6WB1%3DA{*UKzwEkZ$3}i*&H=-i znjGTS?${a=XHFd`zBIdFfC%g*l0%w(02Jkvmz7BIKfT{VA1RrwLl6!W3W|CR9&=3< ze3NB#0|26YrC2amrF7ZZh$7cpWTt4yiku-gGl2A-8d*)n6y_(*;F?h}P6rNGre*hi z9JYpT5u|?`sO@4BD_lUAG*Nkc z(upevpPq6sF`T-cspcRKcOUMm2ya8{Ypzao_jivvu8xjH$D|N)WcokW>nL=>V&W&w zM7$Q*h$7rj+S24Z5A0H|)k`&pZi1&{>tk5zz>&6BORLBE1jGOssku{LqO7?oCuBCQ zJ-Kus4p?*gebvEZm_uI#hrnv4P@Ni9wEN1!#{VCb`|gKt@>O4-;(zRlKZ2s>_Ie^O0V$2Cd!U_HIEnpn5uzA@51sNBG*diz7y>`FKCPr+9wHG2SxykVuG3_MxX&9D%6>x#Y1CQ2j>=AZwH!I&Q zH-l5IhYiK;1=XqfJ_pNR&m8Nk!(YRbXap7N@r%l5wnyt9XdJ~)?`0-m5B(N3`e<#M z3}7%h@863lSdRgMR@PdIVXE&vFVs{gx5B+vRD?-}9|e#U*MIJnSM^B3&Yv;|L9O5r zl4-OS6qR)Y79=hsdB*NF{aNF8HJyW-Wqt75M2epQ^5BJQ>r^IB257#n;Qn-Bc%Pry zMDZNC|D3p#`%cX8a4~JJY5r<kZCN;wPa&lO+0lYZ$rqAdV&t~UQsDwXVGFw#K*x!aXV(`e}Fv^&+dDB_Le zS#0fd!#~MG1LUS-z=2rt)NL`IbG5Dd(i6mq@Gd=K9!Tc^H*BNh>S=_;=T3%6Ur$E= zD))vzk<+SQ?+jUXc6#Yi=ne@h?5Z10A5a6fX)k zPJRBpCsusDhH%g3_J|zL=FJ7}VPw^>9nmv26AO-gU-|tbruY2HMgKfG{1+V39sj+V z3gZpuXLT1b!H0d|D}tu(gB;Kk!E~=%!y9J4&W z^dyQkw2Ah&gKF8cOnwG&K9GjwtFD0>)t>?uz5(S0MlnMjv5%rJE#f;j1ae$N!~hHa zMsa+gdtt;e$6Lp#wVX-kCLGsJC7d03IJ^C77+HRs$fCbv#k(~(I=^pIUr59gwx=oQ z11RZ;lCoF={C#QA6J^U;DxD2C5yG5=dk?+8Pw&wgExQO1L$GF&nHk&Hagej_HiyA& zwqMnIyd%wr{F;X3xxZBF{#Ac`kq0jyFKgjV+|EHD3v$_T8g$hiycaok))&DGD8stD zPxm9TiVONjOs}s|kneMTflOZj0I*X@lC@4Lm#bUXkTN+SO9ceRb7OyKP#qzE7)0h3<9LboUlR)hxeb8 z(Qe5%qP2(iS!gOT7bR8CZ^=nLqy^ z=%XM4Vu}yW?^Li(fJ(`^lvcC&299mdC#3m}(R$ZoRj~c<=yjoS!WX71GhSampxVyr zCj%c&bC}AlgB!(qTp_d65GJyj{rBk7fO80r+{uD5#!LEj?vvhGeIh~!w!>^l8E*9# zP;dPr$H<^uMh7r-O>yUKH-pV{Detf@tzQ7dbpW*YfcIO@=Q*uO%D!T*@4>5684zl7u)v zj&y`z8vwY(S?kq@#zmxfoj0GU|2VmX;d+`V17aZ|`N^(%*+ysqf6A4f;x=voq95SM zBC=%wky<;6xiK7w`R2&t2TdZW<{*laMgd&1#Q2zq7bDt-H3QgpE3*scGpI65e@hSF zyFkHk%ZfK+P`5rz&_)yaf=NPLOojR-9VJb7B2Q1Lk$-j&&lV76BL?vT&t-dNes;J) zfvO3|D1pXhA3m9njuaBE0?C1==~PfsjQ>lv&g8+@)7qNJDL^WK!*i!ETpp7oZ(;eR zdUbS~#F$<+D=3l~xjDPD+A+g)zmPVdmNO^A__vcfFcgfHfrN;^2O$R?B$~>7N(ZeB zD5YE?D;gt7#t&C)K}Epq;M~GxjvE9|057HeXNEE(l`rwf^G|hfQ%DBLGnjsHRFyZ0 z;=zGp)dxG_BJrs*3jRi^YQi_@+$h0TN=|pKE9w3N{t)T9fr`-J4#b&NzjRad=vm)K zZa4kNObjyC$AUWTT-^>(Mj}cB*IE*@Q^kw>UY+7BPXre5P{#zkdI^7m-^I|rA5KdY zRW5Mrwi?gi>(j^09L5{|L^XR%TPA?+kUQ`9gqnkPp^^P)XSM@h73@K8*PuoLK*Mb< zw1JN0@TmZtnJ!&s^3eQaSq93D{>~u1dcm%n#GsN(eZ+hpNBELiywQrKT$FYfkXdvX z@#Ms}PoT;LfcJ)~Rc{Ivyn>W*2I!sR4vfm4UO0%%SoK&Ptnc?AU)H1Va6Q$EK_Wc~ z;4h%ztk4mA`js0?iUAR+Q4>;Q-CSS>s^lYQ_2?!P(9@9Rc|=H18GT!iQ20bJI;{9+ z%b4suNw&-W`U3!$d};=pmv)VnT?7Jq=(0Ey>FHo9x=Xcf3q32#H1TUF64Znmc3nTv z&@~s>1a(4AIGdaRWrq;u1ic9O)F+TFr8;6((+kM__08puuUT0z-+^={pZ#GXhvP3% zCjs;J2St1(*~1DGy$&WO^B0?Cz8g727>v;0mg)nD*`rb(`IlbRyDREKFX+VHTCSGT_T=B zF6Xf$C2XvHwwa79kw47e>-pG0c_NQ-05)t2OG3L7Y?vUy-l;N`lz%Fhpj?4+a_E0$ zCo$w~bIq~h9$=;w9I;%G93x>VyWY@U$V<*#D+&}I0SH^A(R-DNVPJ+$mcYeXeUfWf zv3dxjZ0kei41G|4a|glu0wPgVA*=iA-KnyU!iGGDMLfF@<1yAyCkav|BhV)|or~Qb z8D6KSa%x2QIv`^z%FL)3vOD1Xhbj@abjK8!?*bY`^?E%ec@p1Ti}GpyGz}5&Z96H* z#{+_WoR&3x4ViRf`U0vOSfei{>zna>5XJ0CbF{v{Wq>Y78TMh2M_Q*GU#`xyxz1+f zIsdR`opjLWWZ0JG+G2O6Q7DL(VT_blKxPye@lE*q1Ak&ucB;8t#hphO$GW*uA32Ky z-X18xH6e1gl()t08;;CA9-1W5!)7wl1Jy4%!v`G!-%%n(upSqp#YM9VQ6u>{4xUpk z21+fh(eRQif$A`MwI-YZeg@yX zbp>nM=PN^fNu0W7uP4!4rx%<2H59s9gFs9gwmsaFkzB<5vCU<%hKhFLx#12LhF9yw zzD<(A6_)`?IJ=(DzO1tFqO#g5NAYa;95M3%vxiqPbDDLFN?kgRya&RF_L=rTDwI;p;#X5w&UKd4mVZjlR=w`~vK6@Q_a>9B|lElh0ske_MS)He^%ZHq=|o1*%$9%AKDm349zVPD+2Z zJ<=GgkR5z{ywjQv{PCab z6c{J|kZ#Vk<7Fau2q>g~{YEM|DGQKNYJI9^-+Bzvo%MGg2v<}X5#HZ+0(52IxM$*} zoruH54q_#U>FQZimmg{hQRdOcUkl7#)0Qnw_oS`(kW{bjV{-(=5W^fU_!grSR^`qVr#&j`ByWZsd5O0D6{;0srymq>#nv0cLE|^Ze z6(tP3xyw{l@O`t~E&~d?0Pk5fz3H0q23mgLs3>K0kAMwbY3jYF{MRB3ak!rW`<1gZ ze9R=x(#*_o>L&u)5R?#0|CUj)2pBYSUJKS7^bz0EF>ikR_kJM24Sf-w-u40LY~Gk= z=;j&F!uyU(j!?B}K2r=Nwp>~AXhvr04CgyNh40)`b@aT@m@%#JV->0y8Tuq+J59_}%pkT(NyYv=An-Mv6;VH?I&}8$5h`Vg! zTfY3JF+9yAW5+iRH72@{NyyS3A952 z&}~5a5IVZ-`rQ-d|b#os_wCC~9WtXwB=w*zUc^@g-uBHMv*O*rU$gwk5 zm(BwiGmsNoQ#19pa|D!mJch3R{3sG2dQcH=MN?$eszLP?e8Y+((c|b;l8R_Z=69_T zus2y~<^g5vv)%Es%W+f^@hd1DJ9Nf9`JEhq367a9w>8aax1MOTa!qyU)NS?wnUTYz zRr_0T|L$Mf==8rem(|Lrwzh)~KG*&X&0GUJt|AhCZB+>O7?qH>#l24)Oq4hUdl*qh z@Wyt*{VRVCI9wugR@j$=bLau4qI<8}^f=ql8s2!j{}0!=ZYw{c_qCdjE3OF~67$>P zJcAnlUpmRBsMY?@I|b!dgVf>6YF$Y_&Ro{5@}Emv_iDVG#R6YoKUJe_2P&vjnUPR9 z!EM1F397F{s!zG+*PKh~I1M6$g)_G^tL{X16jhH>B|2>a>*U)>)51R z={^6T-E4K#zjHBab@?EF{lePDTt2Jp#!{}@?tik?baniO?XQA-$Ukh(-H_Pt64+3f z4Tbq^3;(0`!iSf5{QXM}C#&f`U$aAghgSi*>rk-#x>8Ta)-^nT3*@jJT*z|@?Z`kxk zTDFm){P*KlR8W%V3K#I-|Fn%K{2S3o8$e|PsB8e04WP0CRQ_c^#bTI-$UM8{GcUJr1uwY$$Hs4)BdfKp-1} zfp^<=UhuZMu~USuvU4Um86=^jiQKw_1 z%;LbnnTpv2h3wFgIcjNXDP*B$afeWNilW@g)d5M<;MPgh!pc_Q#7Oy`biQVo2vY4% zZw`sSdc5v?4=?`2cxFLCRk#Szo1m8d2O08&SMCOZJo(@5&>1_x%kuemr7thLy=}`5 bXbZ$F@-j(T<*v{2JhU!cI-h;+=Dq(1oixY# diff --git a/docs/images/config_show/stage_timeline_side_panel.png b/docs/images/config_show/stage_timeline_side_panel.png new file mode 100644 index 0000000000000000000000000000000000000000..d769e976a29bf21e6cf8349d631d50502b658cee GIT binary patch literal 89121 zcmce-Wl$Z_)-6hc1}8WK5AMM|5G)YfAxLoF1a}e$?!n#N-Q9KL8+UhixlPV@s$RW% zKklu1b^8ZUo8H}P^_p|cF~*u5C@(94f=GY}1qFrjRZ>(D3JMnZ6Z#LrTj1AECnRMk zD0HZ=qQc6~$%jj@$yi%2@TVqxrNK=b$GgYx_x|FX=4B!v(!n_zI5|BX!HL3R!{)%i z>>Zc-92_q+9le-1>3zTP=BwY>zwk`q=6gM=Skrd+EHeRYDKGF~ISS*3yonF^cM!n9>HRyI+#DV6SKz^}(2CKC++7_$y^&sbiiOs98+;%X3_0^oEEmQPcd|YQ%+=5T~Bx|g- z8F_Q{uA$UUPc_nG@g}brd#E@q6tu`ITDPsdHoP@ZU!X>>LqyD4UJ$;+E;%Jv`YjM# z6Et{{=v9&aqftoQo;y9 z)O_i*W+-Atd%V7tu>NTWl#-~a4(3k&Q1tvFFUnd6z1<_T^>ntDgv3)A4l)Ic+X zv7{YmtE{0_di8hpdXI#~wp|!3-sF5)Bsc+0SwHm(OyQCsR&%Vs6|kG7dB=%1L5E#> z^vh|f*Ll)!(s$>HGE^hfa?2l0*}W9D0SWttX znCpZ>gIq_;IM+HrkR+@uRF%uZWT~4-hMfCEi>I|qwKf@ z7l`~EM?nd?D<3HjYt5J#h52-PYXHx^YT39VGBxBgDpcH%f!84SMQtN5CZgT!UxR6BPZa;l=XHld26Gg?Cg zZExN)x=Ai7-24M8#?mm-K6GrO!a^3?SWV8>`h9xB6s>%g;%XORoQ3e4Dvx;U&zKYh zPK#{;J3MlaO`E36XAHrb1?^Pj%3qRp&iYGW?~8wRBCM>FMlsd3M-a6d@@#H_(lG!UlVVg&M567BLjp!U} z$dfL_a%A62#--No;cQyen>WiGl|3jT#k4~%g9||io6$) zBaFW!6rq;M(=)}aWKh8uMrw`MXj6`A!Bjf3ef1gM0OnbX9M{aJZLB2)P+B$Y1bgnrZDa0XoeQQh28u1zusNuC&nsIdgM2h2R!QTC&}htZ zI$9(C?7P#5#W>M#6vRXo1nOqVNWOW0Jjqfa9;zm_B(*2gx;Kre5p5iBT=w^kqwCI3 z9GiKO%yp5_AKzs9EvCBD{A7|C)+mP8D6y}QA2L#_cR;$RZ>ECSbH;EHgb7O-d_hF8 z7mWy8shTLEYnr+Pl@*hv8-hmlg%E~-k7cW%D(x<*5tb5n_Ebj%MI#4m;eBxJ$M{4z zbAOJT4z>6564uAW_T)phq%G>2^PxYf%+@5TFD(x9#EalLRg>hce#vq;l{rnS4KNe& zI~cWD|KY~m(1Nq5;F151eM}SJ#T@v_-aLCHS&ujR*xXK-uC-NKueGOQRnIyJeQDKL z5Fsr=thP-KhiXSF&;Ffg7djQ;$_(0p&G-)p#*|UX^1GwQpO9OUVCrc4Y_-{c1D4qr?86F5L_$DoCMBO^<*;>65X&%U*Fr+fGJ zTS7*vf6XGl)aUMLy9d1-Z9}06BcL&aL`;@dBd6}+!mdT98;B*oO&P(9U8q*DMl$2a zDVC4JjGAK=-2A1X6p+CF^<-U{J6zPNoxT&MBGhreSg<7O;X{~a*-uj~svDeAOUq!i zr8tXkN(e>(>!{HV&M{9|=ao}O$H0g=I4oB82pkT&z5CicRJHSz1UG}-xb$jVOZgqC!;9QNdmSNzH za1<1yIIO)2WnG`suBouU2#SK8cwZ3t+%FGkXoT3~0RDqy-b>n7RjJi`_>p6QjhXv{sq&D^yP{ERJUjL?Y z6eb&H>c7;3tIsvChub{*CUrJ{Buj?b>;%)cl4hqU+k>R%!nv#Yaj$sre|jzFa2< zirUUqrAafLakF)Pph-EIY2JJYdw%+U;o0&;VFPJlT`GGAy3kofjrQK!GTXs)AC7*S z3cMQnB=^ zDGE+8EA#d@DCeJ$73f2qVUszYp^Omk63)+NL7Uf?fp(RA2!F&8r6F$8!}u2Q!gRwt z@{PD<5pJyI+CHqh+8K)8NPdxOMRD7YhZfW%2?@nIPHj0|BD=y3?K8mhQgy3=_+#K= zfb7j-QwdnJBEttgI&%*Wpv$nNy}@);rX+=I`PikNA&V+AHfe>Hqo12TXQ#oDXu6-Q zDusd_vsV)CZcK|Mpe6KEvA4RD8sQx*vxQOByf|M+YmI-ud(ut2qbG5Gj#L&0a! z41JE4_p?v)()Hm3^`czp&HGmN3OGO;3~AnGzJn5z$QHcmcvJeOn8wA76%GXk!P2z6 zobUTeznbHz#C*3~`@z?|E&iX;nP$pxepA9cFNUdgB*kz<_E zW|gpeD(Z9$Jf9e2@eAHhZshhp{T(Y^b9Ims<0-;zGiQA!SQLGOquX&y-rC@1{iW3 z5FLqdS2LI?5O2ES&<;!vbl>F>_S1FG)ot!9pqbcC3z(%_3chrsakWZiMcISoR_4Mc z^M9!6piLIH`y9#1+;uX%M=04wr7fHLl8~g~`K~9?-G`^$?m3?E`HB&h*5chHw7#6E zWfzT!{I9nj?*9yWK^zb-o7L_)`iM6=*x5WQsT~j_ZdMF$ zLwlbGVve9S$r|Q9+>T55IJ{7a|7<nJm1yXZrTr0d^+f8lzY(j7%g?d1MAja=&euNPo~hXMU=Wp`Pn ztA)!EtyA*j4tf?rpmw4QKN7LeV_MiR2XEa&^;R?8qZWm+s&*S0!MG~y4N^H=8z zX0G?XZDY+V)%~cPd0AO<{md!Uw?*2%!`7amwK}#QYHj6r1rkTHeMu#70%O^E>~#%9 zeXX;ZUTi*IwI$BZHj{mc*Qn#W&{L(j$9WUtrXC@5q0RiRIrn+dT{4%?!A&~TLqZ=m zzHs=-wB&{$TO0qJN0GWZ3^TyU7K^erAbriVBaK?2&HBcY;J0wgZHIa2R+F=`h7e5qGZk-%4ELw5 z)(;=8u;?lRF~8$JD{{jn3P7YZmxsba#hqOUuthDs;DTZLS#MXSA~^ ziXk_2%h$JsPnK>}`}@vKnW!N~+*d2LUv!;5Gaj&;4O^)rkpwTXy44-9I0&WKYC=e> zOEQnxhMsbCZRxwWG^Lp?Tr7QqOcR$?3?Nzj!Q^y7jeE~|3zW!Hv*5=+`9%AuXI~Vk z1U|I1eohPg%N*2xmhq5+C{a~?oS)8%g(^DsJq-&+dn)27SVZA_Pw^>ww``7*Mlr4Z z$`tl~B{UKRYwyKe2_58KZH^;{Dd~$@A9=xt{n|k2h*AZJ!223O4i-*^x`t&s4_&@4 zJ56!Jx>2@V602s6gyx^B3ru<98QdXiQ@nSIU~{%j0wttN4Z-bp&cG*SR1|AKVTl3s zp@Gc&-uaoF_Iarz#&e{Cizy}Y&pwS~?7b+V7hklRt>CS~v$=_EM$hm02^4MUGwBwN zjDF2K%=&WaNsi74P;J^^?P`Yvz!VHDF^UBuuhO0(G@9(ni+*{C9NyI-Iv(Zg<-L{J zpZ%)t&JL23VingZ}xfay7;@3Hm{}pAze@b;=+9Q1-p&O&$d~8XMuvDWLRv63syB_^MQ-exBrz zksWJR6e*{xN&QA(9#PA)= z`;kzF=!cREEyzKmTUL|T-|~n2f$4EYijs<#YZ9(4s+L9DcBK&}B8i+Q~y% zxsuMULEu;x`Xn1qaG-j9l0#ytx+^&IFw0|+qU6XMn#XPo4XMb#C zZI$QMhM6ASNWd9JH2PxZolM?z=@;lg(q$l@wjo1bV_(X%wLgc~VQbf1keu0aDP~8e z%k}=xteF1=Is#WOwtmy<0bvpBh7&ceL6p5M5e$>2zv`QN{_ zQdZ?dCRJZw`Rr7u(%`=;pSBB6T#EjV%nCQH&amx^C$!1 ztc?e~nn@W7*3xEgHqE@3uq}m@@RwhLO4;z<7u5E0Down!+Wq4$E$m$OI3 z=jb{AXBTnNjl%T9O)}g?9KqmOh_end`?Z}m&c{JHA8yhfZHB@rE8FTeapg7|Ar4iH zzc2Z!r8DLQvXg?!YI+rAHhH=cqp(re2GC18?rXl1klTn(wnV&gh<_sb`^qWD)jSn4 zlx>_QRgqs7CvwUCwUUsLNILZ#oIizzPM7MD7JXSY8JC0l)M@g{x;r09qERDcS$5h; z>Y(3UE51mYc}I!yNVegrM>-(Bz!hDc8^js)k`L-Ps5PQ_iu3kBKio=?Tei(+uBaVJ zcrIIuR&)`Ua*&JNAlV!lk?pRN;oVXm(M*6ehJ4^)`9FA}aqMXvmOETK9QA*NmlpDL zPnjy&x5%rLswxyw`Tj<{E6^rQ+^t#^O8iMx%A|tWm$q5$bOwKU{ey#5bJnR@$4cX5 zZX7dGpSyfg=P|c8$+1AHiVm(#UM1KAS>SEz&WJ(6wsa;kWjI~|hVSuc=vl#J0e1dS z#=zjQjXnyEHZGY;KdnN&TGMv(y$&xm|Hf~2w$>r}5-}@GT^&Wli6&;VKZzy$l!!#VAMm^7R$Pi~)oj|}YG4N`)_U8#NUcF~424D}>|9h$gBNwj?yKa~mb5a+ zqh+!8>jHdfc5wDz1me{9^srrv#eLklb!+j5fYN0K;X;| z@laY*jz(^W3qFu6v0TZ{FTO_1yOECA-qRpaZSp@N7KTi>R9U3ZE}{wbQBNHUPH9NB zy>IDJ+y9gZ*hL?Jz_yLk@z};4Q+6~_PHobeK>eH`bH4Sr(mI&bDmdNC%TWu?tl}im zuJ&T{BF>kS@&4mh^DK+EVllDK*(Gi-aQGXfVmMCOhP8P@- zzbr_URfp51KrzU~dR~)C2;C6ZEAOe)gZraU$B?F)dUu@pKW-eZB~0kZTjLW+>E0gi zwt9!W%U}cr9F~7%tKDpwF#1zj{1Q8p^W*MZ#P^!*fsv}|LCmH2)a%4H*`n;3VN2SR_0e%MfXF6obS%ua=cYj5?P{-jQE@7)@~B`3cP};Mv;7U zpHD4_^R?{?Ntc#-oSIlt?E9H?0uH(r^4JdCo5JO|=H%-{mzR%PO$iA;$zQXpT_h|3 zzMKE#$i#w9Q2K-%DkmlZ($5pX^l?^zp9H(dM}~$QC!!E+iQdp2thMfMx+h&$K$4&C z3Rd?^(kB~Ce^ra$nxpr>@GevX+Z!mB`xiF@Z)u4)Qo>Jk_h_>xZzsWZKD&iWh%UYM zCDSx2`Z)-N6>&E$zdsh&5y&)J2G3{LG{~BZxMQ1S70)~DEk*Euftx66E5O+AdgF`H zAV_sYH6Zd928PArRDl3W|{9z@!ckU=#`qott;u3K` zGk6&sg*Z>cymOz82yC+1MS6}zAz2C~AKAX|C@wzuqz{p93_z;~a z$)hkFwqt%d(wa*|RyDxcSGtHRR(y$4Q@)A~g*D0MH)o~@Iab*PRRnJ(g5t+jI%A|@ z^Hl<>v_DG^sD_mx)Wz%YhGAFWAB?eGZo47_5#8-k)p+0c%>NS%%_>le1iCTl9UxiM z{`ThW{^=@}T&jAw^Py|Sj}o1LB}2Q$*tLD}5??;f;6ZxZSy`;DKW3psoL*Y1VSS+v ztzV5H+skUZiEG{BR;oq~vP9FgDvGePYs*6%g`XiP9$t00uO4C+?eF$ZKmt% zU{=9JAuNzD(N{53zJ?+v$%{&8mSspd3bF-Bu9v&mr}a0>J4#J(Ur(-~k4Y;iJM2wP z(6MT)5AYeL#{klLR>7n3yRa-z%M~uGlQs=H9@#Y60KW1oHXd|1_`*WI<}mUU6p03Yn#%g9SS z+j;y{Q~Y8m3nz_vl7Z%fl{7=A@gAYUFheDHA#FoNcbJWD%$Bt>hNM+LBH-^u$!%_z zYL}Bn;sav!?fdS!Y~hm68~Csy-J`FVZ# zYo)Hkwzr(#DLJJ)BBiBl>~=HK3)mU~iEBKJ(xZrhb!M&*3I)Q6xB7tE=+8mTp;+J= zFZ@Ii$|Ze(^Oq!Q57&#^Tsxh%`Ey!|#W%nL5j@!#&S1R!uNM&OgGfLPOEP*X%~BK` zg5xzTv(Nlm9o%PsM|+M))SL$o)#%++8c;le;cX417g&3l@UJbE5H0he$xMj9(Q~Ri z&xXaVNMovMVpfg;S1mn zSInC3p1jYHz)+`?`r3yUW_9FG)Pwqi*S~K$ygJ$0%(+F~vrI>F^ee9hBws&=TyvyNT9Els=NPyHo- zE5DH=Zbx%b{}Pvh{!j>~c`!m_9qH3U^cOotRQ<<$9SGwn_Z(OHS$xDcp_Q!yW z^ZUIL-8zyh>792~f6=Ph;RL4-CxBfM7h1;)kXc_CC%BcO&?2DMrkn*6e7k$*G2Lje#=ojdB1;xNbM^b;~n1eV}D%$(PRt=nmnC@>s#(#jZAmDpVyrrq$|7@b{k|DCh6!+N~2 z7<9NbtsqLvJk0b`BCE_4`C)aKckNV1x=K%a(OFv0s1i?kYvT&5w|0zpP>It@BaxTf zvi#m}=L9UaMM5UyI@Md>skkwBolVYg8W7vc=z}p2W;iX3x>FRs>r;(t04fC#T2= zYauJOdD^?s?Q(|J^UJr>_oLylmZjOp$xu@@fhFP4=@xq&mdu+=e`j!a_BdP>7xLu^ zClo6`c?bob4idR9qG>j}NH4o7%@_c$7~CYWt6g(O3YqIHfW_8s!T#goSs{QV6oino z)>gt8b@GSF>KtFrS<1cjMn4u2Px*~1JMo_AuVicSY_QV+oT-W)Nd&Kw2S_j z5=M@v&EyINiypdjksqUc!q;=ddCI?Ps)Zg6zhmqN|IB!Zu!Q~=YQvU}?x2UA%k1)4 z)t$$kZ?HI4U*6*4XCLkDdr)uT;4E9T8-m(7Eun_XpwjqaX6P`C`L6b>YutsPIgIoK zRl&+$c}u`M12{*p+Mn8`ytol?mJ^?Pc5#$%>*a*sb*hau(* zJ*?ZdWX43GZR*CP3@7bZaxE$9F>4u?j0wK}B`~HV2#07O3b9#nMGbGIE%1QpFvx&4 zn-~4P!^LOD>#yje6=f3@<867_`Xtz1rXzjJ6pop6gulWK)m_Swsz>qmw8AUf`&8RsZF_#Wj^o(2!0Y4t9NI5JvXtGwJXFYVUTy)! z>vWd@M$g6NIOy1qj`h4ex|emX8d`eAf^D-ko>(<+!{;^y8aitp&qs7x;qv}a-H60) zhmvg4?Xd|xz$DME*i`HONWK)p(JiF|kN3vG_(k5GDwKe7c`FSB?i_u}85c*EB${#2 z&cnSCoDfppf0l49_4Pj&Tu!8gNJ{I#JI_@--f$VQBTYy_-W=a-y#sGlNVlM#5*|#R zi7Jw4$9L&N9DYp-7(7aMbxPC;PBz^6NJmAsiK{_?v=pt*+Js&%5ETi;pidtRrRLJ~WtPi4?w zx>V_Uv^&8MEvPX&#MHg?3BLma^516b+Oe0#-5V^I1^xsWt}fcPKCXa#9?%BFfNLbA z*r=~x;9xwJmR*|N-1}f2vt9Kh?IL_7St4(k1`@{piaoup*pzMSzp&2W^&Vyhr$^rA+|a!{={h&L)K*+locWft=v zKVKZK6iA0}#c{Cevze>9821%eN_*n7LcQVc&gV@rD42RsL2SQak`*~>j65*^GN{Be zqv-VHgF^Q$!lG8U|3|Mq#;OlTff8xqWu7gAVwL`m%~Rs6cc_YXoe=fjnBNQ;Z}oV8 zULresjS2k}kL(&hLCyHe6it0PYo=SSuD<)O;G$*Jlp5d&YP0Y_#WkBEP1|B4x1-1y zL7$2p?c!0EJv{iix3KZGj8WyRkd1gmct&zK{S+gK~5B_rJ8Bih)yBclSmU7)!4smTgw;F$`}>SXi`^- zAo&}KXDd&TRYj0wsQmPf<+U*nWlRjEJscGo*s}zEvuInQEyaAV|CZfjv89NSC((&F z&vr=_BgRPA{A}-o#R0n7%Y$(fTTQzoLzOWB_jq9?NTuVJ??SBn(L_Vz3qW`A>@oK3@uz2*?=2(`tH!Yv2 zma0;k_`fZAJ2C%hoKyJU?N^+3)J#$s^GfOCRR9#r>ruGmjg&d%m_mvcIaeY|$@)JX zkkgJ*{)I+T%*J{eqK*gUi`$uhoe|jo(;P{(s)jYcH5I_nCK^2dk$@#f$ya4f>BC35 z@c(HM%(wo0jWl9GzU%(q%l&T;@c-J0K{$+>2>*EmnfGDX|L3ib|F0g&LbpVAvx#OG zJF{8CPgV=HGC;W3-{X1@XuZY{s<@Fkotf84=P4#y2=Dh_Qj9gYMz~& zo12uBl!N0c+J7=n4(WA$6kmHgmESEjIwT2swdV73t+-$Kp%98fv;FA&W0L9u*8s6I zT&T5zM1%~H%SZhaOTs!4ncvE1`9EoNo-nyE+&%<=!tT^+luo? zC2}35<#Rl|-OEcW)v7xlj zB?pI!awK)CGx?`aimhgK3nt*yr{kuR#BWsKfzw0vuDV6rV0*iZ?GZNPVHz5m*Nn06 zcwLzM_qMjSu?!lDqcA!ORy(69(s4}K4t+~bD*_((_GZox*GExQGVfI~fX^Y?PATq_ zd-mk(z(XkAjJ~_OD{H+s7ZK@X1M^=%biD*_SAF`|fX=0W!hg-c3N;4{b!9svE*IM| ziJnF@s3&t(CM&HTsQzDog;l`*@!B(6=$OLiblrTn5iXQ^L59%&@9S8v^9Q~@mf^EA z0>r}W0ypkD<9`GmsQHcq*~-|Myf82cTq63{MQ_3j?aj~6FE3|Fk&Qc!jg8&5=M|$Y zP%i8ZCFC-j5G_#FM?E_`(|Kal{Z>#=a5P^N1#&!^=O7~!Rz~v`!5}6k#>dA;M@PrS zb-KSaYg%F&jngR$4+|65?{ggJ@0WDI1a9Vl`8uj;!~iQbU5~5!pBwj=ds*YA{3lZ1 zzJ0GW96%F#{Kc&nseyw3X%kp!V6u6dPbc8yY4fFOvq=#yFt^9k6-3wbZtHv`S0-t& z3yA)nDA&_p$?MEiXsNGmyj74{Gb@g;u1qM<= za4JYTC0!v)e8uy@s$dU-N!KOmBP=YuPCno2(WIvQMKw*p?b@HjGk}GG2~P)WTlLeE ztBAcQy$?4oRzw`5ma6J~%nv(KCi$pVax;gB>n-zYCz1k8P$s zbLPp@&B_aaFwx0q$DPqSKPD_WPv&ciHhGF9zXO>N=a(Xz*X09=4twDZSBezSH4_*2 zgGl~q;jox_ygw+bQ+KaG`>z+U;@Pw_4vEoc!CQ7cV2qKXT&mVA@kb*O-y9trJWsUk zk04Xic2MD|a(z7Ot2kG6y{^udW zU`hxhZ`$19$B#8kIsV_Xs{7;uS5ry=-pBc!VL6j4IB3?Gb2uF@+BW{`)dweWfz~>G zVPRpn!27^LdjfT&S)7WBO7Q7&5(vWxxq30}rygA0?u)_6&8@8;+0~lO-)#pdg&t3R z6z@PAw?Iw`-mXG3#MmElY2uetDfp!D*yS!7lgV&8>?@be{063E4Gi^Mruan>eAuJf zReM2jcej%kr&*lJ{d1iV)J%p#v2(LOeUMBa6cEsl`461f(5rRajyQOpju+}ci}gP# zCz;c2g5J_9#`14>z>{1i!mW*5Y=_{p)mkl3^)>(b^QWxsnZU(zD3+0iAuh(Vi17U? z02{iMs0;_Q6%^-mI<$l3rbOs56##^>8uYH}f{$~7X6fd0De!XPxWgNEhaW@DulatM4V+%Dea=)o zy_4p(Z1=4o;o_{1OtL?vEbFae(ekc){^|q+2Rpd!kx!q~SLv7U8SFKDZ z^bf-d1OBg^1L^^;$O+aj3QU{(04}Mtnz7(*e(9q`^*6XX2jHAoz%}~N8QJR1Ig3uQ z$y1vlMm|D5n&vNR1WHywT71bAwOlTTbyX~ zddW+59lS8_RaZ0U4LK_gbEO41l1mQ-^VW_@@mCjsr&3Z=2M3+)&*J0HvO#5qgc!Ol zIY7R00f-e)Lctuka|WL1m;fk*X?X_Aonz1P&#J!-#3S%=Y!YR`&@TJw3gX<(8Z^OlsMF;d!>5aAE;X zL6(K8N3H#u6rcvtdA}+S^UnoO*lMZ+O71yDA2P5)KKLfu6ofYd^=o@LVdLO{-n?jW zaS<%^vS|0s&@ek+HZ{dKD@bdt?(3%CJKQEkMb1Xr*4p~IzROpD zuTY}6!RdipYXIs;=j;FZHPTOc4RIqm7#U@h|C;p4eo|&uYb4Wk01{HG&e{ha4G6`y zos4mrMD|z0A*J)J{+io?v&bOO(U;(ZloiKMgFf1Q8&mVZN5Ap8EzaU;UCqzb)6;3`sJ!}orE>_^wTn6!v zu|ZKZ#m?hGCUT^=paAUN03f%gr)NOzQbtN@z7CYObD6(9gv920xr?joe)daAf;P!s-A{V z7hoX)5U16=a&)cUxy=Am3KLA>m>t0X`CZO=^13fviOG;t$6I)l`CV)eW=hUwV5WX_ z`Nh1T`-+@(A~8FQ-=}%r7fjJwaVD9xi$f9f*k-#|8A8=+mZajM0}1ef`4w-~@ox0_B{zc5(=T+SXd18LwoR?8Y#C9g1I z0Rlq8gN+TJMS$Fhg1^x$vQ8LR4REo(p^H5qd3ieV0^m}k#r3K{DK98En04U|G@P^c z11?WhdV2aMxz%F*_{H{CoG#N9g0JmQL&Hr#vr>PHm!Cr|s$kK92oSIP4F}$v*xG*j z^aGF$ya35Mf7?Uie%b*rv2HO|fY|{&{ptWB*BBV%{sOGT&px7A8~`%g2|i!;h`6fe z`8F*ay~_B<15J&RnHYMtSO$+c{%4JK8}2az#dBpt;unB>;|BZ-L*zm)U{!X2*#;97 z@K=^-@q}wOJF~vW{q&dL7^sN|Y99BOk$vgC+(0^4ZvXoH04#ExDg zEi;V5&P~%pWxdHv&dRNjX?bho?*0g{4&YVQaoE*=l_4s^9H`}&z`91y|1OVve0&5# zFE?V`w{uFevZ$)eg{`S-a7;)+)f5t8#j8IC5H56708+1j&t&0hs9k1ZRPiVc zJfpCG&j`=F0l={yY_;#dLpELXs`BdVxxcaLcLy}s?YPc(8X>3!)Bqj=x6SHjjSLLm z6}RIC_?X%Aa7d3~|K``PUnx&MC44ht{Bf5J0pNTLkfcT?CUHVrKv8j@Gm6V#Ii91R z?s>IcrodjeYw(+Ee{b}!0A`FvhOf{t^-juH9I*^1s4~ne*2U46()lf*xK&_#c*?vb z0n1|i_V)JjXn~84j_%_}(X4Sppi1TB0HyR0U`p6&UrkMSbK-Oe06G8RL$N%~-zC{( z?w|Wp1wmlI0=V9vE;8r~1GJuI2`4@ML4PC#AQ_nQ?7yG!IPFasjin0%>J!P~%b38L z4y&42_L*b8|C)L%qEAG4TG2E1>2D5WxIPB><5` zQT{K#2mSxE5vU?iqDqK`wYjlDkv~6cS^J#}@L7~u|GhV@g1pDY%gakaK_MWpv}S`) zNX(mpT7qm|!^_5|^r``WDFQzgVnpb^+>Z9gMJ_XTD=nF;bH3*>2l7ts}9|Cxe-mHK}$Mfp9bVpNdP zu8_*)xnc(~t^oJBVz3h<)AA=d;0CYPDp78L3b{NGI?C#dA=!Dk_O>~Hi}s@E%fW`) ztif^K+Sw+E^}3G0tRHCTIXKMQB-^&Ny;z=HEAr9?bx)IdbE|k8qX26UI5Ow+Y~QE@ zx4Jm)@gCF@s1PHXR+y5J%BgH1goBAG`MJ>x)Ne#&WGvB|K&c$_xk@9T?p(RHkQS1U zkB_4X-lhW=DrBa^cfdlN@f=#KPQ*4Z$+r;s@@2*Cqy-Q-9ss4CfO2#yXJg!L>+I_C zy5GyYyV!X(PrLv~d^l-40f<|^lmMV|`@%_t91mv!U;)I32S6V#3Qtx3>jl8Vz##C( zNi(LwTAQxW9qP z56Yn~z-cKRH+|er0-eTCGR3#Gy#Rhx&Kf{_egc{Es{EM@n8#3nAs@8@XayKP&wS_y82^Ep6^vYk%GXWDNEpVkv<;FfsLMQ*X10>;a&b z&j9rcsH__DbEnpi>%q)p*1nGTiy?u59Xq%n*}Ta)z(Jn+6UU%IUZ@MK72v*%*{<=N zHy)HqOG{%aqb%D;`2j3=9$@#Em-c&9f`Yei0gnn-#T?MpKY#xGH{gwqQWS#gB%z1H zDqtyA0ShvAf;l@kcNsv?Z*7!UyW`Mrp!aaQsG{}`%GyfLpQQiJ9bM1Pszi9yet_fRx2q_xAO@oOYp25Y`wC#bQ!@L&xoT1wFv=a|;vJCg5=g$dxw}le@by zI5o74Rv`z6S;u{UiuN_V;#i@$OaU8o#=J)8=~77pa}i*v09-$|y*#veJX~j>Q!id6 z1K0f#ehn};i4J%JCP3K(MOr6Or$G7nb|V}xt)i{hD*zWDD4$t=9(WE-QUZrOpFX{J z7y^7oGBR*H>lcDRhhR-!{Q2`;BXGHzR?F)50wk?LfxLk`U>a@i71{ij_Dq@vnZiVl!u7?W2?_$i&m_p; z^>_w9FUr)g`Q9A{5CT_#qnClN?Ag!5#l9>H@Ssj8K|yCH6cR!At-+XC7}Naoi;JRL zcc4C)qeI*H_;rp0RAMlJl%M~D8^0v!?3bS9VZa&Al}!mUJp&w0v}vBhd3~)Q1(j z?cqCsm5t;4>JC=Fnl%XjiUtC*7r>a!lM^9;GhzvCtgo|>m@$(it-hLCEPCz09to)F zh*Loqu;9oxSFwTNA)LTBU=}mC-o9m|0`j<13m|?Ccs^j}PL@P$VwcTGLhE{A!8A+| zr>IOd#cGTHdy?hkfUgtUIiCB{+980-2lhvXV0Y5fNiB)k&-yX-=)XHmtIQ(ey#VZU z9h%&M%3lsWF?1}5-FP@2pn&j3o5mxFU(pIWVXyBwF|UYP<+9;0MCGe7R#U>m-?f%j z%meV{>27Y_uoVl?@}UuQ?71Q3Ie%)`YjS zU&`(7uIaZE8*Tt=wM1cyd5e30L&Hlw6vX2_6aafQfbG^-MG9^3qElZd0!ZngfqbVm zTJ)Xr)ErU&zf{MCUVTMb#sOT|=ZyQy&eNSO@6n^=H@gc*2L@bj*ZcuQWSbhOs;Xjz z14+XOHO&0E$-{C`R8%}Y7K8q6|Gzjp%c!c_s9l2s(ukx;s3_8nG)N;zcb9ZXcS}hl z-6gQuNOvP0(%lWx-F2Sr`@ZMjH@-8@8v4Tl!d}l>Yd&+{_ciCjXR1Q360x$PM^#`k z9>6_Ah8BxQP)6(b>xO+ad`7FtXYBPoQ+1qai3u>~TC zIejlk^~);;1hR)?uJakpnIaw6?u&-tN?p&)ENHK^qkH@?N=-*8XyLxQ$jd(y{Aum= z`nM4av%9Eh=k3)=sfF!OA-H8@fdX^3}gDMRO<=J>}8UojK zMY{o=Jj2dD#cZEKWBJ|AEli`89QnYGK|sd+%WS9w%P)1zmf`~@*iXt?LzB?e@7A~J zbq8}n)NQ^ye{o(E=@2Cpfd1T<8CL?8iHWI|q=ZR+jC?9tY+um5RhtiLCaxTW4`O#` zcoR>UeapM^VKh`!tA-;{7aqqIx#~~i;w_Blm^o2XV!|$OI5-IF?}1<@)+s6?QitY( zRiKje>igFQ3hZQTqeygMn8BE; z!Ij3;6?%EJLNSBMLl!ao2~pj}L+gw4LEAhhQ}X*ttZNh1X7! zkfi7!L+AVR^`NSPY<#SRDKTK{y5Pz504afLV%8jpntk3?31CmhF66&7`IN@<{Ak?# zk>ZA&cLzd{3?+8oJ6v7Y&(x-uVDCRJ#ZsziNn13+z-Uta0smRoJ+Yf4HWN|6tOT+f zfKEKNM z(Ps$l_0WNYfHbWphNAaI+hice>mLoxY}ZR#f5>$6&UA! z)*<+)iuzb&e6)+}E=7K)-yq?Lan1ciM72&P%lFJtFTSra#PwR4EL%Bz4_udWBt3Zr zw3}`-6-3oV1K-RCjPu<${sUr}7c`pJMQ3Zu{tIHW$0J#li>OdDP;LdufaedEQ1Nm@ z1!9p*@z3-IzuDk$vjbB@2nY?6?oScr!iAPqsQp%Sek*X9$&HblRnFK|YtVnlJf6r` zc(*(4ChZP(oG5a!EKOQ!q#ZJIXRveQshM_TVey_mO~bv3KP&$3B0W+k7^WpHOca(Y z6o_d%PuMZo5kdf#wIaO#)m-2Vy{%9-7DypL!s0p_QN5XrLsc;*EbtW9gVZDd;}tuvvtzR&G^8Bx zGx-Ex3ZQ45XEMUVl{$1#xB>&Nog3+%R6DPQ3#>Iy4~^yYFUZEcw4@X7zcilVQ;Prv z!DlYeSXcL@ug}Mx=hSUdi`N%?d?9K};QiCC0!M?lkWT&u~v;WS#<=5Tyg2 zavX^A=OEcdMnl8d=Q#m8aG%?)HZwCbOf(k_T=aU-Vinf`I{o+51C}(p=JXsoa2vndWux@}sGuq5=Y;Y)9GMm2lE~^Qc5KY_7so z0n|6;Wo5!1K9LV2k_^jlvpr*zl7eS&mHlICWKW%UOS^#MlD)fYQ9=IlTV_$XEwM84 z`S6#TWh4Z;T}+ULe?3+U&uI-OM?HXi0`?scDebpLazI{6ZN7qthX3-_qQj(CmO!hz zU{RZEC-+mDJ)kS4P5QPb)dGXABuDmDJ3ed=%UKvG%~AfT>Vx6eA1L+BnsVD8mP+pj zN6={#B}p@G0FOQ%nq7nTY7KZHoDSQ#Nhq-RSqQ&uB^;??$;eBD3o>i0H@`tmlle;2 z>Jbfi&habGmQLi4S0m1oXo_m|L85p0BLz5{AQz`U%hG3NEzn9qq>yvi3EN2rc^{|0 zCMzUpNi5#DkyAnuOX@`16GsPDMV_@Q4<12>a+_AuDb?Bg7}u0@zLc;ziY=F2q-?Nk zUgo!HJP(-54L8~A9%MYw&~C8wc`CiSq_L}ZwBh0P5$odliS@kd&7ZAFszEZ&ovwGGjzXy|1R%2sSL3Ol36MY{V-`M;VcPEO-$UH zPUKX7{WRj?dWRvOZ`UO^%er|`E?ovVeJ(SOT7lJS$?q+JAZTDJ-8=l0 zl*k9S#|&m4goWSaiv)hqFsM$xC_b#e7HQX&>XR(Bqgl*3MH@Yf2MAIg%b-<2v|Uon^io+%0`j8|9qme*LL*oFc!wqr!8Cdf@`4 z>_{&+*rZa(Eiq8C!2DTIcJ8V$-ypi;5k6yZC)wOOk39x9>LKs-@spGe{cj;5(Gh$f z^d@LNWyyVLaxVV-*9y7MdsT+Yf0iJVV|KvS)PfX#iAgix2RpHjRMfOL7*ln=c(R~aVVI`4Rf)HbPwULL zG*KMvbZf*}+dpnoAxM?-7&ra-e_82d{4zz`<;D#*(tCJT?dt2QkFcU2pcewP06SPZ3zFFmBH$pS|N2%u!}DGwkCC6F|_C`b01p zHTbTo#wu+p%hU#?Q&q-_Kv>ah55Pcc`M^#GN0u_8uGYoTee`0?cC~nGKvQ&K>8xLN zLtiKEmxpo2KSnuUf^OnkIZWz!X%^C4iP&G83i?%5f#dJh zL_fb?p|(ramEu@XQa=z~Vf>DZ1oN*}4nE(Q?s(P?oFuM#Hg!clIe#{Dj=Dc{M$~8w zxz<)S()iX6%*j^D&z^)Y%62A!$#*sClPGbc`ekoPp2e=(uN}wm|5e9cQ_IlpA%f-; z*a0#7Um3?rMOoR%mq1sQEK~-TvcNdj&U9CoSn(v#4xtQX<>ex0{|bm|YHCPd1%^6& z(Sghhv_cp_I4M6qxdDU1z!@<9FTX1mv&DzS2#s20IDiudoPonEQR0cN;fI=xGEQ5C z|7J+?@gUR%a~}VPFq)D7e~fJm|sR+;NWUfXD^c z$z%^_ocEndPESvpp#Ek0KniQk7L8Xg`e!or{i{2{sD37=FVa5sw*^WxxfjUz1aP&k z=2;)dJg8%X#rSX%`i&}+Hn;zd6jky`nl9!0u9vnz{^tnKXs|7dYG@QwNT5B$bb`n%~@%aC`o z!~?^Hw~sKO>F4Ves1wqAL>1IkaCXQ{koTZF=l)&c((b53$k3vlsmZLY7MQ5m*8kii znec&2yM&<ea(@5KF;1(a@taDH4?U9|#E4;-C8fR5Iope{5 zrGM~Z?ARP5>XyKfejKmhdT8T=pX2>#9a1I|_ZKhvGOMNzJtx%KXE%L5EStO(o3k4S zgQ;VjvNeM^O^qstk0rmK*S*sq(eVCx(;0+ZoL9o_*DQTD!fy&WqF7TSKi@nx*T21! zCEc%E{T_MXXvo0vC&@*+^scm7-ZObl@!w@>R5sYe-$>kB+#5)%NxHG^<$|LKR?H5w4_8#@fd$bW;4H7qA3T>}CS+3M6v{o={kk$RosJqAH2UEiJ zm1_1MebG61GnmE+|2=-@Z0>UmwmS?@?5Zh}p4v+;a95fSMSmsgSXVbb7`XR5NG!@6 zm+;?fo|F_Ep4y^)3!g`D@8+`nC`5=$R9$X{*dhn%pTVxLp<-qfaOA$O3$&E0s4bI@ zsLr;Ay~R)LRWf^N1b=fey|eC1`eB$RKs6T_y+oFLIsI#nW5g=RoH|8Tr6?y1jnJ=JyXDg9)- z^R!(R`Vf<68BwI3U($ypl&qVl(p-!WBAb{*d88n6cPk~xfzkhh)mg4ZNCWWi)kqF3 zK6%-wUOx5^0>feD?vvOj^T_(`Q~k~DI40BRQ#_PH$Cn$H<1Zg(zh8BBcjKrT=@f2< zkR(Q&qD5~UuAl#OFJIl0i}K_$bT2x)8qZ(UAyGI&oQhLDJ%z^1x+3wjzu_t9zCRnF z&1u#7?{emv02{O;57X1W7V;X>)QMmMhP>ja;fLJuBTkukN!9fPn3Y(^hde%muDP3Y zzExKYeW)wF$|#lzK4IH|Ca_QBkbElkEh+&!)`@2d6@9`lT@bWDVDKg4aTk|K<9VoN z|NBIJH2aA|i!f88xsz#Mu5#_~$5RmF>0hszqWJm_%+#FBxOeIatUjDi1vTkUb3eXL zkIM>#wq?HU=xKZbHi3M_DzkAYP~@m%w^MMQAR#&K&v638NtW&8;^JVcH2WX;8n$hi z?J|pRO65O&oJ44RimL6%!%sChLH~)pqIQtHFPergaN<`T=yj#zjRQSW&z3=ADAaP3 z%p7cqamu>He}9<`iU#RC76yhdZ2=fSpc(W1KW;(lfMW!Bbc6^c%sjX8=B{&7-w^pQq}$ z#Mz{wpCpP;rs)>4u{i>)*T!Hvzix|n2);KRz9ew+M;K5WxKfK!?9$xMcWZ4{MXByu zW%%#jK%iQn{*4E`C4gvxgM)uCO|g2_q5SpG>WVuD;B(c*3zc;orw#L@Q(GCSJ^yt- z_!S??Wp*S0I*Jw$$bfNz0gV{~U)RL?(|GGaQA*}@%ap4L#CL-T-rV67&JNeh!(|%f zpHq!)?9lqArlz;)Ho#W^5?&%`I{Z3BlK5$!oGmZ!fiWKDo3*y`f#A7k_8K9r_oS;+^7Z?g3+kou94*Uta!DX0BF;`pu*JgyiLT`DK`zg^E?rK8gOQB{hGX(`wlxR(P zf3ysr3!u=!?;v{qOz$`|kkoara`LGMHgc5N)7YFqr$0mRSkp}&+*!kx)$ z^!&&Z2uxr>NGHCz$2fp41Q-Jk%pZ6uILhzjTRnFU%g=MpeXrqX_ko(R{>i)LVd*Q@ z%DiuYciDuhmDPTW&*Pk}QqRMp7H;@9;F~tOUGO>XCj7fN4rfnIpz4sO+#f&OLER6O znpjkC<$^OHuE<|u#x4UMaRIRKko6Uc;5vz%XR!<5zWIm+14`uP+CI3tUIR`Q{KM;F zcN*v9mj!m@Z=(|cIjWQy(6x&z;zl{h2?H9F2Q^eR0N`OT>s4Gz8)tKKQwP`#3FuU4 zzs{-`J=-2l?ZMPlr|mHmk@*$<$yY}*JVyv=$Zt)?OtC$1aB%!@8kB$IuS@ZuVoig` zpuy#Y@gDr@R1O<`(3b*4aa-UP!nhsO2gnQZVDgQRPt*;RmJqY{!RU(7?02FQMLq zhGGo5C3sy=FF_=W478a9(J#_VcJ<6P_+vB$V9s{|k;R($$;Zbh=ojJFN2`FwQ8fD` z*nZuQv2_h-#t(2pYBc~X>&`kpmH*vofxXFt^+nXvYO#rzG-MgLmffpZ#2iCVB^rb! z5KmUvdQFw{Ml%WbS63BORfAjrAS*%?aT`j_TE?1&yDp2H!}LUsVaYJ7=kaXaPZd9u zlAIr>XoiJM79MZ(Dy&X-Cb29rOfUo_5fh#kohMeh3e`F)=~CC<(Pa&c-QGvfzSHm#0b%8aE=!_JHa}-G7yzmnIk|swf&} zbANl4r3rA859*>Bt&DNM%%^`KI&u)JV~HCg8;mr9-jd@OKsE&Rj<_?vl!++00MF}3 zCln`WJeemeo#?1AS(PEIZ;7m#ry2|!kStq1OQarP+dr#9 z#$a$Ry;@njUDZfw?+D{FfG~i#7LA!pSBs9E;w*Z_LDL(a|DP6M_FIkU!*JQ!bcd?$ zXI;groJS5K#JsM!C&l6ylTWZne|L9(xFu!MZwHT)2(NE^Gf?ATbQ+!yF!A@*-p9w3 zcRsZTcKBD+LlB38f4hSK46H}J@`=2Y9N6s!q1OkefMJT0GXrz1dkebb26-=tUl$RJ zfgrF9wkwG?g*<5tR8;j^TSLGPXge<><_878q;?{HI}5<{A0MLuRp)mDr;h6*$GE%L z{6OL~CujS70T2)%ls@wpMmM;3#@|9;12^R5%?+?GMUp^#)uZGv@FAeryafD!1X}2y z@O2n}&sl5({6*CSfCheK&yz|%2QYdJTd6$~^GeDbf-FBw%>zz#64;-C%dx)A-aDU0 z!rj-aW`NuRIzBI1KjrhdZ2@dz*2(Elu=E>vqgh3mkOYvddr1Vx18_V)IKganxG zyL~6_Yah(aAYYW8uA{6x0itj~osNZtg|#)Kyp^D!AmEvwWl!<|ryK~Uv&E{VD}YOa zFcxdochhsg|_Zt$-NwqN5Xdo!ND>U~d9J6fqj*LZBA#Sk54D09>A; zd^)r}kTa+gz#`{ivuW(nX`2d7T_k@#V5gO(q08` z`^fNJAs}j0XzD6}1_4@NQJWJ=J%hMqXa2J5wVfaYV`!+FZuYsiC>}AsfAZgn=TV-u zLJd(o?Oi64vk_u{4S7-D)6Fo7X)=EOl`**k3`7x2U+3r8*gkInzqlSH&K z#Bf#JtEJE1hE32hFhnfQ-GkoEg*R&0yuWim?1qbHu{A*) zlNYUZ4aBp*Q*n?yeVT9po8bw~m%@)>O!J30Ad!{y_=pWQ@&zy=8* zhAMonM2UJKFo$v$)~z{yNFul}C4R~Yk2kFO7W~nH=B$W^y*LVjBB>$>tts0$Fa;b8 za?2U_aV8zW#YpfH%U+v@{*9XNf%*XT15pa31+bkMz_Mk3gG&|{09_%^h-#(M`R-Dl zApNamIdcb`a0%pUQgsC6aEPEO^xaeZfX0>Hq6%GXx8!GJ5+ks3eq!aJ5vpfi(&wUFLnEthLgS;^Y|=zKBw{Qw?j{ofA=*5QXbk++gw z|6P&Gc*9OatKk8srBSHdbpTOd{}AhrDQ|71eEEpOa|7PeX}JVC*!hAd7rr12UNv+0 zM(fOdIAD5%S8_cn+&9!$q&TQ|)QQYklJoU{>IPBO457CYwxyrMUZQ6s)p&rXLWSiM z>btf*(=NHURmPq5q=X~w#$$KTL*?_(QNsKoqOkb+eDv@mk_W$0GkVS2TwWIGF>HW_ z`TP6(iU;lP?*7+#nia4ND4{Gcg95-^<`jrCwiL4d@99)ep`)56f9z5R69P|~yx>+n zI{EZDD>@sNIJG8AuNQEnX!zL*Zwbjm*dIO*O53jGe5(WOn$Xi)!}`CO=j%Gg%41^P zFm2WT^|>#SyxNC1k|WkY67q2xz@rW;(NKEDRP{q>V8wTZ?!;dfdAVImkFF}GYi1N$ zfa3Gd5^ei|^WICesT;`dO&5jf&-}tbQOsm}`G=a7D{tP6vuxmCo8b42iaKU-0AoQSxn-HCKQAJ!9e3j7ycKX3cJ6t5Q1kUF$7HvnjFb ze;1Og;60MAn0C@t3w2w@)_z;UL(b3@R4@s7GB!*U@5^1S$*Shg)_nL+1pf(VBhcMy zYwhqWOXb*TzHo<6I`afJ4-a-&RWiW|4bOZT!?tvP$3qpU>c22}5uAtqIJY*S63M0E z{(FJ{=qp_6(9_0SIsac|N$nEH91r zb3#+~i^5U;*7i7Zb8rz?x+U4}?rIBUXkkzA>(^$HxUqFjCWqO5R{IMt}2?YT85{rjpPkVACs!csubtDbvzW+IUdIx9wPs9dWv40sRRAV?|4+gvNzraHM8U88K8%A!4&B0#S58 z-c&Ja-aiX|{Lr$rK!k2}20?Ht+UZ-J=+=w;;6Fx^Q?$GIU=bC$DREm>ikZ|bt~xN~ zFk(>sGbkzGdG(?wqLK{TFn$M!TcpUxKwVds3)w zSjtUXW%jXo$(l>bsJ>YMMAb3f(np%c`K%bwTS+CXN}aS8U;|WslYf;Wod=QbZV5Ky zY)A^)M*3%=vRvtTcz6IaOh-pYMb)zmDI(&Q+xy#<37FzcQc$eSTGjWPuL00P2c#Zy z*a{Z^ScYr_! zwO~|AisrF4=yRAhPT#Vx19;{GFfw3u?^i6oDTo{pkj+Z|jhRrmM~7ffr=c(M_V$MH zhhTrzXmGaaaRasUb38nN9mw2)`n{&s>){?G6TIBq8c*32cSZi@@Y~d1m2;GB0;@hj zuvQndvR=EOk1{9~?jFVCR)7(BnmCI|)p!dRX-TSFx z6yvIr(Hv2uOUCPBkge-wqm6t0!C!`aGJOE6TDUNC=$i6zGSydg%FtJ^I~(hYaK)z6`hr9v(qIQ5O0d3)-(s|Lw zfRIlIiwi3yU~Lrg`7kUpC+XyOA`4SGsHRBlg8I)B{DfmPpFW0WcTnCFM!3NoWC4t?%O>(8Z7SX0 z2(}0wwjVb$|5hkg^d%%v%M3wQ z5oM4ny$L$`K=(bD(>_p@_#eQP1{#A5NDP^cL5&Qm?eHhB-B%XCmm0ttC7?WG@TDOg zsj}2)9{^4z#T(}XQ^2ai;7^b&0ZyQq$qPa8jrEqgf_mXO=pgJU)5|ZhP|>vM0IhRj zOTp_@=I~D^Z!QiNLHYsOcTk+>LEAY<6?;J&oNql~mO8-*ffZn{b(;SojvNdVT?Hi2 zD@c>jgyrpk;Ah=ijj>#&PK3KssWE-@ma~cIYN;T}Uia;`^qhklB@N6|lSNvZYFS0L z-(I`(2V>4&*WnGqdDF2ictqf>Or({s;AkEvaWIE-glN)Xp0~2f^%J~@b@V6s!?|y! zI=P0c0qo)t?1(7chs}UQ z06Oo9H+0w0X;ihJ=%Z5zp%6XjN9==2kJ=pGuinIcTuCLB&x5E<{9sbsjSh7fd^c1-_Cl~5(cbN^)wz| z`=2PIKuPCk*(~v&Q|IaQ3<%||>FH!QX`P6dk2}73KdoP>7n&|tJZi?6Yfu1bAKSf* z`SewLHdAEmSM)cL4*V;hOQVA!W>g$9-qi208ZW~_F|mRr2iiZ3b`RCW{I2SM9V^lC z3+G!}gc%+VDejUT$9t;crNwgDNA6*k$zu4)g*Oo$A=E1s$^L@h@^B@>thsQh!-fb` zDJEA?Vx=?BupL82ZH;>TB|=O!`|?f*-kaVgvgHmD`gbm|5@b}-4n)YmjWOoeA9!GW zN?Fn5Y&JPMJy@iX)9G+q)m5ekpQ0RXFIye6_ux^Z&cMpos{W?}?}59tP&E?^7c;rw z89hU}dSE*9atUmf3e-qE0&vq*oK2v05he=8c)~xf;nZ(Oc6l@xGg7QxxIa-i3rZgt z9=t`r^=1JybiMQ{QY%M7aRdVaKo6UUzp_N;reEW)D=QK%`wyH>fItt*g|U_w`}0gs zsP24wK67n8(p{=Zwx0xKlQjc%V*<;)<#3k&EhRenV=xn94V3u4L~vuC7-G?7T{^yA z<|jvkS+0*uBrHKj+0=z69nvEgnU88}7B;I@bNkp>E{*z)=yTk+gx&>GdCJ4d=cLuM zlEuB4e}>No%mXdw>@TZt+F>4l1ydyA6-@i zS`nYjADF9H%tSoVzrG-Cl|&oZmH~&&RHYC zXG29XA$o4RI;xXEr_8f`_T=3GoC7q7Xc{b*60I-3ioc3oO5 z!O9xAqR%v%KFV8Ud$gar;u}Ybj}lYVQo`VVqFdxL25^LwY4#}Rlp;U61}_=>=beIi z`(nz%QOZ8(p&21ug%Q&EOm&^J{Sps^lkFQkhSoG<*8+K*I*Ms5403og1DO-tsPeqN zMH1r>mS_2+>Oat}mb=3{%Bb91HM_(O;5aooH2aq9DPK4|i+hyUwjEwC>bCNpN~C^( za^eX`h#lT9y3}0z-@n+H8$wF(>W@n*KWx3&N}7_w8=jA<8Z`Z9JjinsBQXwIdU`OL zAQ^bRd{<~6<%x#ODv81<7#T~zXp!S~p!oIo_O`abhGzif0bKCczp`B2!T5`xKul94 z?Co$~0xE;+`R=!5pa{$VIa9oU^Z|5H@+=sYe|+rhbIJbtwV*^_YiivQCD=pk7rv~` zHWO5X_h9*31#QC7R3JT29n6dx*flpk-kM|+%^tl-4CT?R&+LqWOpy>6|3YYx^8!b>DMR z(L%J>!(`ip3Qo-bN%PtK_kOnsgUg-8Nza=V%X+MIQRg`0@Ger0sKXN-4W`@Qp$}R! zwd*q^fnTl{Go;+Y2Q{kH)JqPuo9?_W8p%kB7%gwTU!>n_QZqPNk(^wWvxlH%9HOWY zMp8?Kr>Gj2)@ujXKon=Ao%F=@kXzGvP4g6&l#nt|a{b9iJrH>+2hti8xcJnTUlgHQ zhTY~LTCR2TY(VQb-gb-8E5?!Yqx5|xE(&+_*Kpgw{;Z)g#I`Asue6rt$XhI*jc&28 zjug0~BK&*=3CkvxZr5HH{>NxQtll18fNmbu;|J;r(TkAQ`2LSd_FqKwxPm)T8-hdI z2J}@%%X%4zdKTH_$zBpuQmNe89DoW%Q6R_{h$1_nQy4VV%Y$3UCyBA}-SSpxfXyvxMlJ$t^t9r-x01XCQlc_WboB$`K}2GcelPtKrgYQaw# zp9+DB6)|Knri-I+Bg&yhO$hWSC)}++NdTfG(1}Xy&B%e501S`et#!?Bd=jnz7Ar+KfwsCzMs?W7&-U zR+t<>4tKFX_Zpbyzw)zlj)*F$sd*sgvlKf{BvDvJ3r@5P=6td1-F#aPDU2_fB)(0? zlF9|}x;d9;zZ4m-$+ITI4@;mpOo)|{QcaB=#@&>NmIoRKvVOBDB%Np`PT+Ynm8x%P zi(lYy;MKuR%8gP|5u|gL^d_eNI1I5jB2~B)fp$1EICucus0FaG zgFm1{IzZjrwydBl8t6R;t6^p`)`mA=sP%?@tOB`w#`kA4W_uLLW@1uX)2)I?PS5w_Pw%yH8WHL9&TG%F#I}<3J&cs z66lH`DUq%l-HAFXR+0nVmWx@X(kF|xT2o3%z1CHF;|^KH+l#}fIqvdBf|=LVXG)oo zt&H-eZ86~`P;|2`#do!W$Ntt+#GH9C%c+C(Jzm_SjvSg|44ZFzC3o#pu6%4}ERFL@ z>!nQ{`H2xfJ0& zb0E)SF{+`LtOfIU@*@m?Ou!OfIY-b<18bEUi;qQDn)bZA23(mb=n|?)jE@iOP_4Cn z8vqC}z$n7{yd_TrB1NCk8^iZ{g#j;<722MVESg=aUZ|>w;3cPBt))-m#Is&np(v5C628L@4F;RC8$mHGTueMB|!ZT;aOyCQa>M<-8cSs|Na zy}3X(H}p{}TD+HcYnfp7qyv#Qcv+jao zya{cX#BUy~I5zMYq#^d_jB9(~IMyuv<#dY^gyX_Lph)!i%!A@8+@oeGVsbu&WRK;z zX=M-1;@cB{^;H%}qx<4PF5B-wf%B;dy`GYGausLdv^7^MU-@QKIK}e^0>>6Ser*WO$t-*8xFW+l^!0L8TqpP?OmS zJWrow`rW=Y*W){n&eMbA)e$L;^Rya|l_7|zz=&aDC;Fcm9_mkuFO2o-n16ny6pyO= zDv$JDq*SF&68kCnr!9GMJm(PFA~KH8x|3S1N)VeQ2dSbsjX^6TsL~-2NJ)#14NbSh|*|7 zJgzrdeB-iQ+vMSghOHuZp8okER75~LjpVR8_vPRNl4uQ`=CSn9JU`XrCLk@p zmdoI?Sh$F{CdkrSua3!-JFkLiUH$K({F#PSd9NT>8T< zeU)nZ_2Fih)$>5tV&wu+fy*ly&w?c$8Hyhreows4S_`LL{G&2`4t}ds=jRXPdENGCF}V?)oau@1$|Pw*OkC`y@mN>h=lz1Z@C-@8Dj*`^O>0dBN4`(( z;0&K*At8J%4{fcl;mVgc^&kK2luj6G0u9}MD|o<+)bgQeAybNgm15 zqCU2_lwURS8SONf;@YEpE>w%i_q>@a=BEuQ?QwHsF9dB@6~o++&7%qqif-5B$>}8~ zoXy1aEe`_HfIPOsO6}jjDkjrg?J7mJr9$Kt_1Id(S#02?W{n9`A)NYWEe#xZYSkr+4#s(?Lb$KR!wY^UGF3gGlRJDNsc_v9 zxJ7(&8Gv||IYdoXGOcys!w zsB5F6o}wyw@aDSFSyV%hYv!wF7hQUzHulB_$V!_eqLrIevGogiP3vjyvWnaspKBVd zP0Al0po|RoD!K_k-KJ@eljlgAxA+IcUNiP~$322Q@?Ni2CT31Nzd;`%R*3W;bn1B`(p; zCKiQnFLAY1q4#1E z$A^Mbl#xoZ?RlO3#GX*pRP2YY60Tep!D87u3YG(H@J0v;l4;STPm4R$fXKC0N(jN_ z^BEC0-^Jw7%OuW5W(tCr*o0eAOcklT+uYkY#ZIzmOVso#S8rnTDENhct=-OG2| zS^2gDGKuwW`U+gNJ-oXDk9Tl5a+b1Mc)F97;$EH-i#J3MFZirUuv3$>&`tGaxT7vs zKjjtv7Gei8c{14mLrH?;uAZA+4jilW=1V$vs;?^K!ZN@vBh90 zjplLD0p1|Tr&`ohETfgWKiGl?NBy3YP0(-KCY*Jd&Rt&+n|TjdHuM1aUF4UcNN|7U z+H+q=JpB#T;KP#EBDbwVtfwB}{qYpZIFF&Fn}tcs6=>XE$6z3r=L-Cwu)Pm1XqAOr zj{S;JTXP1e2K ztuC1~F8ehTH5|&XNi>g>zXi18*Q<{)7g^;DAAfI<920OxLs8L?`3X!$C<(f{O8mgM zRs+WIa8`om0D`)nlvH6fNyf8rESkxmWPG2!A*ACf@b?74kl1aJY17`q?$YEUP4x*a zQeKPH#JpNKuZQPf4`~M~-^9HxwoCXca|c1f>j+_^Nni)0v-C?ISDzJ~%QmyzTS!Hb z6EHyK3YV>cE%FK)EuU#!mMory6bfHcWQ&`Nl68`E8m4&dccKMug393WjUi}^x`V@B z#_KyG2dMrFw~#kxHO|QPsfN|SyPy)9rLJho(vxK;qc+P?uh(6-#^0P$a9q`T1Aes` zM&PKLtOc9NIA5LFw3x^q!B%yGiuh}@7DDeG#z{OKoN5%P8Z`#i8(^+;A_rDZZ6MA z82{4D!y*+1cM{+S<36_Di%lf1Hol<$>vj>yIXl<9%yJF>Z?%aK{s*H8imzT+WLi+= z3y}abR$G=zexKqB-~k>foOFsDcAW-*O#;Qo52&(Gm4><96TWx?&T9P>$?q;cxZ zy-AUl8@JaIeZf%Q7ll`h9!`I~nY@yU_Fo&s8wojX$_i1-WOz)4qFj-K;7`Rn%%WR3 zZ>Ys{-eWcwEKM5Q8qcRbw6%3+R#T0s|2=JREk0VJwA2ld)4i+0{c;2p5lJfOY`BTc0Rh?xLVoW12;pfZlB|jdX|CEwIpe z^7UeAL;_YJ6J~<)Vb@OU*1too$0EmqQA={wobqzI&Nk&ROHTxNF=vp$A>% z&R^AQ{cdxvW=}dHzS(U3Zomw$19Oi>OiUmV5puAbX*^OL+uLFO9mpBu0{ZLY|8QZb zn4FGCwRz`*<}%>n?$P#^cUleJ?OO@!XpFr2Ym4i*n9?cxx7Q#AqXk$0X3)HK?71p1 zsJqm8kRI-=9awRh{`|Q z#<4AbzN>Qnq%HAeIF5Wr_U(JjMlF)*q8JoKvqhVTr2Bhfc#em7Xu@dcTG z<4*W<={$qwEPl|PI9F;AsPQF{J}BsqW{iZPu&^KGO`|p$F)?irgq6(;v!rR3!(aV{ z^{N=@DlTf_uLk39<{z@1)I)mHg+RC8&%HnsjkA;F+gz8)^xSbB=Y;}H%iN)x#EasV z65vt;@809#BDDNvfk_*&UN!1#==*HZ@D_%Pa{ytT|G^%wvC)^8=fnv_cmi~-0bRzJ z2DBct>wfk^?M8~%%vm4NvF2)=p=M}Eh)8==wRUGoUVtA9PcBriw3x4p1?@KFS=red zm9`#CT*TqNzPN1T%@xgl4#qqI_enFq);}OX-^zUAXa1!Sm`nu*ew2FNxq>mt z^dEpzb$53M3K@m_&(2zae(Wmf+>w%&UI44o?|9@20lpdlX8H#P09I82X#m%m03Sb% z$3(IGI8k>u7n>U!ar}m$E4SW$%NHXiKEB!sWC8Rxr2wR=Fd2Td9_Z(XKui5T z4nknY1y{gYSCoC?={yzZe7G1T;V`7d56R}9es(@v{!9_ulDLaKo*f6qJ+R#?zGAv( z_uSrHae=bCquF_dk|PhK5>NTG|35fbq^nLrE!~BTvHTPWZ4=a0a+Z%^K@v z&TKinZ7|k#1XKScihT3Z^Ze|L4tUjU@5qNi2R!Dl;}m#jU_mGJ07-1jjLp}n+^9dC zjGrDb2g=+4asxxJ-_)HTw?K->!(SJ5b#~Uti%yqNF_mr>XQDTMB^M3-5PbxjJF?iT zD*dxyyg?Z$DtBn%ORtxPx0`^0doql@N7CH4g6UTa2c|+APgK;6+1h_yY>$s^;^FXv)SVto5?HoS&aM!=rlxk@a$0nG;^{L{y!ez5cdSb4MFp*qk>PUDkF_7s?+i%7A8vnUbzpC)40R~^ zYsdyX=L3v>(1cf^>MJmE_p!dHzP`Su=G;V~#_H-Sz?7-!@!jb_OFkHo9SdeLac#Ck z#Gk}XL_C)82mOQ(w|4uB=6(yH0sEy?;wK74Ho5#JlPv(M>hs+*H$@BCpK{% z>d_t*Z+3lS=Vqk2M5i-dz7p#bmFuv`MVI^VyU&V7^)&dPrmKR4!A?Cmw$s~pQM)XM z7;scS7TZ;1-Otm~f}pKaOa_N-Rfaq7(I3hk_r_ink#T)K{GQWieUkJ|CFjSF0ry>$ zkPiLJ1@HH|%lFG)rn5SV(LA|r*B}|{8NJoIA3n6$4f2qXkg$EZ9~`j&X{aJsD*d~6 zd27yZ27!9+FX#i=eV!g!NyKgydamCd3pConzzn}2t~bvri!~%9BzmT+Oh=Kh+O?Dm z@J>D+b1Zk*4P457kUl>;5^xup-~|Bp<|pZ_E0*^YJE4Z3wP;~V36GeB*ogiw2`$1; zWJE;4tyHA`>%R!Ihl47<#e{b%Zz-+%>%Q^)8rciF>r39R>09)CYkb+Kx01%|7Tl{q z9ZPAG%@Kj&qyKYq=UCJBlXoyzT)D%}gev;j7v8*f;u3VwTzW$MoRlsz~Um>WiriRk?&`v&b)o&3$;^o!eg?HjDM? zXA=+*gj{C@0DvXMO_C(yQ}WsQIv~e7YekYhKU_QDe(o{e!#~jc%vm@57eliA7#Li2 z{}UCt3qjhPYPK`3gnu(68Q^8kHM}$eMBfy_Ug28tWfGX2xh93~d$IA>3I%o~HB7E! zket4_YyoV;7GPDcIH8f}>qgHeV4i1lnbvzui9dO9!EuR%ajj zJ~J*ze8Z&}-A;9xkMRjhR|&Cq<3BZzarSlqy`%VG|ViwkX58AR5Y8!}#hrF4Ym0v!xZ1%i^HEhK4SR@n>{3YgR^iyvE zSCK#}&Xteq+NA70 ztkJ7Bj-TWXv7Xx9D)kMb4=yzjU0uxX3U_%=DJtyev@_vwvCnHFY%$T0I4SgF@sT?> z8Wa)g2=-aT%<~t|pC6sEFVa2iFn}yGC(0Jv_pLASaS5*C8^}tQ$ra#MGEASb%YgZW zpt<=a_OeDD(6+(E{PHnMDntH!G!sR0z*mRslBWjL4Hm9NX9Wl4exmZvdNu`y#Io9h z`Rv&(sSKs9W#`!{oXB3Dz_$e3c8;Gq17%m;^#Bm+P2A0-iQinmTo+Bn?6siSq&& z#i@@zfwICx`ua@Mzdsm4J9qZE;;U|U44X|4;)iRI*(Yg#^htBI=eNb-D6;AvH&4fF zj-c`LFf`smdCKhaqz8HK))I!ZL0e4Y_CYfP8Ta{eAn9DA@!;D=S9+Z${mK1#fBzz( zDp7PY&?^Aunrm-u#+eDne)(m}uDMuav$j=J)6(Moak9?!a|=CKZ+ehY+bfHTA~DCZ zhO+wBfLufVO52^q;1Sgws?+z6($b>Ro$+*L-;Y&_=42Rh#cFL~uOD!1T|*h?N6@|6 z)d8L!xk+Y&+IC6->g!YRZV9lh_@4SRv#n@4J9D6ZKSppC{lUPR2_{WSl73?vVZ#P< zig4(jF=SDow&qChI(%}z8Zz_Fs%8y^mKi&Dt{|qq9Z2H{Tko2osa?z3*3}?$6dETm zw6k#HA=i#~hpRocS4F~27YikP6(&p*!1Lt)ko6TnRd!LkM-fycR9Z?vq@`0rr39tB z5v02tEI?_bq@_C)kZu8KY3T;(?z-!!-+%wP_spm>4xG34-tS(|dZK*RxXK$=T+Q?E z2|qIhO#1W+QUc5Tw(e=_lX(Lancrish(J9F3H=EDNUVbRafM$QV)U)F=bOkpT+I3ByS$jH}(>gSd-)UH!J{~ zy*a2#9AN!T2)(U;83jea%OLfh=HT~aw!($^qJnD47F8zN z@^%JvnW&f;2xB+_Qzh*<2k-CxGfdwI)HCtWy%mijH=5XVtBmT^YE-nZZ-27AAKAL( z1j4*zPhblq({mm8l5Z_?-maxQ5ZL_eZnqz|!763^^r^Drx83RhpHa6BubVn6#uJeb zJXL#dFxN0<9xEN;yDwHBKnS5kDJ`HEW}K@yD49=Qk{vQ3N$XDwCT+-mu#DtQLUvJ>J)^sVe)DI|fP8TQ1z z4IXW`;D2WjeJEOLIUZ8&T{#lG*hCD5XRNAdCRk+$z@XOFGHvbRWlg}{n?6=Ki2!>| zxlU`&c-Oe`dt-UQv=r#sRAs0i-e=$ZNZgC{6;uA^4324G!okdqeWZCCzq2)xU4ywC zQbrfyr~<|+TgLt(BO=Z_0cGGH2p+B=yws0+pfytAumbW2Sj-e^2sr0pr8onEoAxu? zsk%$yFM)ohW7~DOAijHt!&vt5RH+3V60t1zs95i zB8J?#k5mGBg!P1tX$jgZq7{N+;U)&g_I2nYTSRtZVOM7|?M1k=zy3mZPE(Ek737=- zI{2~!X!x~+dwP00_u2-Xb#-mo~p zah5SNnT%=o9W~amZ&#qwX<9}$o^yYBh1*J%=6$we;yKDuP4?Z~9O~LG2q=lbB_n$< zaL&2WbnB4kalYm z-a3?%%A0by3RgU^t!`T-mYai~@J_R9u>Sc^`Bx%q4qqs%S(A-D6eM!xE1_sR+?rK^RQ32n%xaC+Q zV#-Win0U4In<}U1jaEL5Nd__RMI;oB7VRXIP5f_c=^rxpzp)}@fsS#W{pyqN1{L`%%*a_Vn@BsSPuOyGZ z&{A%|Zt@$=$9UGKo8`<8k#G&Bq!gO2aN>&M;~w5bDF<~ef%27#pj#q)-c5Rfj)k^E z+TgklsF$fy*dxs~Tj%l6NluON(G?zjpep0gwDh(=mg-sziY5n|15M z{f;vBoh?k(pfU}dz9zX(T2*$TE<@zK&cd}T4&yEQbzi-_ml6y9)3ZBC%GvpS>#)O} zKbk!J(Ea&DDHj%-5pTcOcXP3TafYn)M$N->6BSM5~M%erCHnCP3u|G86ObC!x{)gYIc2 z6T-B(gn@So_WFa8JB5-?&3G%w*g1Bi?~T144vkv=A@w+k$7)aj`cQwhFz=TuV`?xy zHW~^R-u7C~4DwW|)!h*I;=IGJBtpFMfr$)?}|F}LBJk!-eaElCwZY4eojobbE!|Al9k;Z`Dkpva=gp+ z_nKRZZ!l3m!EFA;K8xgl8NBo;U_h}v10BMDN|~CR9`wQ|OKrvLmzjAwyn7Lsv-zq@ z=BG@%P1cq&L{`o-vHj7DjNp+wiWKJcOzTpr7xBgD0a1v*m@1q>z(u2g)ZwybO z07E3N$k$K9Dt>p;uQzMpr2A;}MP@&0v30z!=G}Pbf57vehLrLv|3S|cw|TqA2j_a- z{CVg>tQ$IVRecW8lu&R^e{}LFaBhClru5sLMD6+lO3u4R<;~F@L47huBb5x<>>C&E zT0Erl9pnZRZ-jIXNH5A#cyyL<%)6x-HFxQoM{i>tYVBR#E?l9Plr{pM!C$EjW%h>Y zE??cSMEPWg?6D8+88MW#@lB}PT9Nw1o4%ECgL)b6#`eljCdRJfx$1aJ&eC>7(sZ5I znh6-i?zG~%{BpI`=px{z^AL`Yd{SP-Ag-;x?&L%t`1(nj_HPQWpM`T=j_7{l9=bfVcc8v7 z1*tAr`LbI7#6$rWB31SMc6dq9(!Tfeqv!Rls;W9XbS?-5f-7q*lEWR@a^=bu5pi*S zj0LJ}Q2V_EMgW+4t~t7irZXQ&n!PI~D{L;DNM!|uItb7MKb!t{N)N!>0BFvn+?H|t z3&4c-S}$J&UuXg*^*PS12Jaj2nm+~*DLk(GBlWX&2!AOE?R7_b#Rj_oW12=f4w;b< z&KK4e!}6j5X^kZ zoTyThRYE3%AwN-2n^WQ-g~Y(0F_D%136ya*Fkd{H0IN;#0GQk{l$2~k>OZGJ5ugdq z0@utzDZp(BNMEm67nGi00@w|F|9Mv00m`p>Q!!@wE+7eieqW*7e*t|UlNZb(%ICDu zy=Rb2o|E;;Jms8BKNZ)W07BNwyqYOJ&OzVhU!V$t50onVo9sLP9$`5s%TX>o!NeCH zOJg0F?9IDa0sI5>_qDwcFcFxV1QRYw#0CV>&0YCO#BLY|e%zw^E*QzV$~kAi*Z^I< zEs(vF5G#8()8w<1bC~e`?QvM~9+&ndYu5;cJgds4RMfYJ+llBNzb_d! z2M=nqD(wPhFt|=L>Cfyzrrm5#)gyz=fRexMTseLY-(CZ{PB15c?QG>|`m}$>;zkUm znSBs*RWw)q!&3~KK0;Da6^4`w>~<1k{ME*-8!-V;@&F~j>`6SAMT}8d;D--KAY#3l z0lTLQVDlr*6u`X`6BE;7_$xKqO{{9e8oeezFby~Z6FfT}5^bj!dQb-jhtg0(SDd9?fRA=&dnJ(h6nWG5XPn` zLQF6GG7CO3;6&o~2V8C)mwCK7?RRrPU!I?vQ~XxRc+WyHcI%Pc7Zvs8KYq+qtl)Uz z_O9}w*-Nc(qqLxuP;;b2WHaCmi#CBa^m>Z)1$Hk!)n{M4Y2ak(Qi@J6;NEbk6V!OxC0 zN31^gEzvU4s-ZKbPoMa`&wQ;> zs9O)o8}wRc1WUN}8!)9KH!!iWYiIlz=3?K6Lk@Do_-G%Lhu}qnc~3P$(tB>nrRi?} zGcfSC1w&Cvlx~RLpq>F9u1RwMQG>?}>sQ*#EX^r`r%*Z;IOI(2Ae}6gV>|ED?@NNe zsHETtjJVX$-kz!J*RMM}A9W*V`xFf}o^F+PxIgj^EV;_zy+bV;U4!BjUvU8US2A8A z-(k=E>XjT4c^XxuN#+eM;5fKBZZss`rN`V>zIfbNbFqIz&SEofyvS*?xc_DN6|ajU zaXBNccMb+DWzwef^tSO+9BYve379@bg~d}Z->oTl|Nh;>1q#a#Xpjj@&&Y`3JqYHe za-y(!DCcV;;{n|UL)yVC#Fw77H=QKH!^7i?F717)K4t6sU`8S&ust7D6}}_+Ot6yU z_J@kO`tEuDhx8N1bcM?42K;yLBCdNuY6LBtKbpyW%wD4}#?67Zc)H1abO6azNE8X4 zUEP@YQc`xV2A|>-*ab_wRVPyL+7Nua11N*l|6g+pJsD;7a~Rw_N65n4kLl#^ZX5 z^VhG-d6m9ZT8D0VKxj~d*8UZ_w3*pixjnF{v1=m#xUs;SUMHm}*2;k5M!y&l>AwF} zEb?M?@yq=$kvB&!HZIO(WI2%n`mm5PLDz$lXD65yX9-tvaAxq&`D7@rP_`J6*LdF- zO9{3D8)ctZtBaijov%e~oOe3Mu?5c0iJ6Hoo4>z1jA$pI^4wh#GM;7@!X$JbUQ(dh zUmXi_k*=Mc2x|#`X{cB%SCn67yh6`&4|gY;``}@B`b>rQ1_RJC1Odn3tK`go51~TX zge-TNng5+r4*K?v3X@Ji?R6EYekfvRhrlPa zAo7O+USVOkpFC-P@!4c2GRZqIhQlr%UG60tB;pRWf403fsFbq#-ps1(D1&!0H7)&p zMZllK-?vTdwwHchJKzg$0n)vu4oR?Eqb@tX-O=$}C5LlZjc7VFFhVk|ar&omiLyU6 z6_dkRj8bO*iGYtX!Y{zDW;^W8td;FuOm)HKB)p5X2n{qRPE0wmd7Fkncmju8J&9mlV*YAFD5GcO_=gm-R-rS zX#+2b#(MDuyD?whzbr)%UF-GgN!ISFtwfS6p@+Lb!8m< zA*Ci8ty^*^E!v+ym?{e_q4Ig1hYcz`^p?y+_hZAV6do_Ls3$M*m;9R1!dL9GQyGJn6=*;O!N>FqNVSS zbU&=xphmtVv$1IJ+@Pe_SmVu3Htf1I_>D+6aNKjjM^c}JUu)Fy^G)>jFPPA(N3R@U z*%UK+;keZ$YINVEzn4c>{rt^hF{uwd=If@1o_3w74myof8sN?_SXS-rKWVT2JmH|? z{%;mVuT~M)+bHF)BwSpmRsdS8{q3?tX+z=B-2Z;=HlNt79PbFst z5&HlUfxkNN_#4>K_p=$oXY_g;h>b`beE~_h3^#qe%wubxopk_6;F6O+tH_P;8Qs;O zv*@(iNN6I%>$7y>VeXr>pL=C!=FZ!2&m=g-T|%}b7^8hZ>AMXAQ)j2M*(f?osa@7+HbefX{saD)f{!z(U{8(U^y zpa_2nRM=0?M;&jr)h4jZqlxHZ?f=Ix?j7S_S0=tX9(U2s-ut)*Dn{=oFG$?Y*0(wv zmSlgQp+{BwXEf?;1&PJrM}0bA#LGvw4q#deDJny}Cc348WV8V4C1ATnU$-#s1w4={ z@9HSwx~vHDGw_WBkIC0+f&Nq)1SQ zfl83P!7f4c^T$fLV!fPnW#>+~Zu=Hf_UC3`*!jeZmcOqtS_pF_VLj>K5Fb3!I(uXD zdrg&oLqrND2YejHnr_tH4)?<2OUa+zWG^j2o3=+U5j$rp-drZ(mPwA<=x^%ryEj(kxf!YVGc4hMh4$7WXJp0St z^q%RESHsF>!>aB~i2qI7u^)5&avqY($?g!}SEIH;-E^U48;OiL2{SG_#%_+B~V1(>``;99M}88LxLq|*8dc>3>TbBc6g=;=?J{$rCyY_k_xK4Z?x=|Mkd!o zNVd2jA>FKith|0ix@~^>xjO+=Xh@y}B-lZ(2zuuI|d`Pd>h!kay zNAOn3{y@pZDN1{ED<`Bs;Q8O8sI$yTT2U%4i;gZkY3N4wt71+ey1y0T9+D6!)PIsXF}fTLIqnW6Aes{HZe2V}b!gn~>9S1B7j#*mhH;C_Wb|IMuk6;0^s?OmLC@%V-KM4Ks z$CT0BR@%@imJGs1NAES}c#~Q4CxQ=d$$%Lo0=Krd!kIkHfD)XEw;ucoj7vwGP>rF) z*K!)?gh-11e#Nms1@)%v`z3mZ!|{p8q*!ztccY zJGlalqU(n%$JoPms53488C;KauF)L8Go=iz8guqPz;xhu7UCJ z`}!eGJE>L^Aftgv59{Y1V`Mc2;XV+MG!DL%@3XFTo0fpjS*@7d0mNG&a|nE}NL|4d z!mX2nifR@Dq{Uxq0bOjZbdn4o|9+r2(*e}L0G*UCr6ZHQ--4ULR0h}GNA;<$FF=PeAtk1yc z0Zb!678#Pv8mQP5FXScIz(n7%O(N?pEh+hAy@~BE+A}Ivq6AzN9gjUc)BdNk5qy=O zmN9uzx?4GidUhFpUB^exGP%8I9-Cy9Y1~j};AuRIWTbOA2GkBox-`2dSpp-!Sd>YB1&58tUWMn+iD-WA&YDSWMt5Q2#%Qguw;KvFsI^Dc}w1QD^cqdc!Be@It2 zsTpikM_~;AxUKjS9qSR-HCoCyl$2~QQ(loLn;UH8tyUkRx(A2K!WGn+dK_$9}%H>G3&R zkeA`jSCYmFPI0}B79?cV4zm`$RX*@y98419Mv%s#GznpeoUdNz^XJZ?a{d> z_MPrG9H;y#5aXZ#2kXix(r7?3X<%Lf+UNC~5#p%92_k)E7|>U_(8xQ$BF+Tx~adLiVS@-O2G)2|@-QAZ6wpRBEtA(A{(NUQ6b({2N z0~Hq4XqZAo{I6V;KFnm`o$}!yFbSM6PfT@YaRlr<+7NbQOTLSKx!neXf%NEhTHSMO z7n4ZwPEW@rGoB?|F8ha zOMQWe`x@YqzI6WRe&azbNQnU)k60?#qzwB|z!E z8+)k?T<-Elt9h3yt>%5tu3`-rxh)Q}XG9xOZJW-2;(RWBn>6Uwd=*~=IwQY*db%!= zw-rl{`!B5%kuRTp^<4T21&O;!B?@in*EB?{d{wUJb6vdpyce#=en5_m_9f7yosSXY zrEs+5KzBS(2LkaZ=sG&O?YI1^(@zT41Evn4l%lj; z;^N}$-FNTZy9be#4o14Kj9zlwDVlU?N$?0<9(rMmMqh^rn9!wF&n;VdF-<8dO(N7* zg)8#fhKF`?By9Fdj+(1;fmS$r)f?doW`cGJJnfwEXPq~k8ZEZCyuIExVCsM^$^Gx`GwG37#Nx+uH+l!??ZamTu9lLx{)eV0UXi z#RxH*CegKim^b?EWWLnkS>y%lYG0^n;6f*s73Rh2&gsz&##0BAHNJu#C-g=XG|V&4 zHjZW&!jj3ldjF^k>7uF~OC!_PsMT3^|3tXiRwFDu!y0xw`hze%Jt{{&+n%FjFQoXo`8bhi(tQt2&}^tn_*bS)-iU*uq(Su8`6>_ScaVU<^U__vfwKsO<@pnngAb({jQcm4X3ZALotJvI z^`n+rIV%sCN5F)%N5PGGBGs2YGJn`+Gt-yV@gtL~$MuX4 z$4%Qu#!4nzHd21RhN_G;XclyVN-PpaBo4=>$76eyDZPxj@=ag)QIPk1ldWBA5m6+` zxftgAMU8~cDL#YwGQW{&pjgt>8Omj2KB~rImsy{d1C^|Si3!1Nrj2~f8vDh4rkJPC zpFfxC)KaO&Nn>{=O>XQe%=NoPn~k=ll>3AzBIN7KA&NX>qX^06W)ncK8(?wd)cZoZzb{ z^#|y6F_L*DC6zoMlT}L+2Am1GWxkB_eC%}D3p~*%AT9PL*GHTKK2X-xpVqtnVOD%) zz^csls5c-WfUjVna;2&c1Vo3q8S>7P8 zn`1){a?5aZF_1-UB$bC6j=NB2OKnZ9iOZ1tzh4?ouK$vA%Oq~FEi20?P+pX(OSG9G z9AAunaWGGI`;Hlp^TQrJCy^$auJPTWl>vIx6W90;5?M(hm)S|fW&}K3uoGLxb+03D z`V@7j#^pjv9W|*=X}Vf0+>dFGk{M+~rxSFm($@A(T$)=He3Wi8iMj0>ppA*aI6hac zluMW>_ZU5x3}jZ!{PA(#D>W@ctz*HehV0oIcfJmesLlNH-p)eT#8;Qu>9<#pF$u>i zI%CPJCp{L5vl4^Jip-nn>Az@~yStnWc0SK?x3lMx-$Rm%jc}>rg z^+sYE*?!D_!AwCt=+f-4ERepRg2h}%cE!P$<8z7 z?qc2w$kM*fEgu%f!v^l?|*di-soSVKK`;j9ibnfLn+sVy|rH_%Ji?iMtRP9~6S zJeBEv7omKJ*6PBuI3-v&?0bShLCh6rlwlXjXQj#zU?BK0JVL$=n-6bn;2&Ia{66us zdJrs(W7dv^eFSnCiu)DKn_N(bQy#>8>$blqN;DG4O0Je<_AB;)IfQ@K!eV@VFz4#3 zQT2wT;HeS|gsl~IE zUVC$sfMCV-Q)Fc1(J&*n9d_ibb9@G0ZYm`UGgUmI;|n@!Mvua<@-Eg>ZsL;Hj>>t{ zm=kf5&nURO7g3qn)4aN-8h=;c9^jkkt~bN%Ew3jNG$C4oz_j8k@iZCoeVT7N+lZ@F zwOe6~)}{3Lx@996A`bnpheULKXyvqhhWO@9UKBUF-gC|{8kn*WMjXd_sGX##koUGi z({460E9l+m^2fcBJ)!410+9!uIaX|S?&c8!+i#3Ztp$^E)y3oKe$zM7Z$B1g+wUkH z=gqjrm}$oMNolE*X7H0^N3WtK6=7Ij)H$wv;%X+@qX*PyUzD~IJge%(aZ!y>#OKW; zXGH@O4nppv<#6wyFaMwq4}UFMFUjPjYI) zFxsD{W{#2?Tw`QZi`O$?RPk*g|v##AlQ>&DKuekC{2UJQlewz{Dfsmt6SbxxF(-k5d$VTuta zlSw7`=FgiAf#8mWC96V@q1%aOBB#$5-J;XUVp^Q2p29Fyoq1z`dzGn0zw4ohLiu>@ z1F1>(Of$J-u`~0igX`%p#ptn&3SF)KJFbjn`L)QsQ!InmeiLWPJbDFo373L?ZSG=D zw#pDWb@XztFOIJeP%s50u&pIr9E$SZwj8CDcC8lDc^Wuhb=l%p#`A{@KV*jUNw=Nd zs@P;qRwB-Low#-XTovMnH;mKt5%c4i0@aA11LnJ_O$vk67va229%rSwPnwoJ-i50k zQL572IC+^M;4$#21G`iFqA*Ug+vN(6ygpIOS*7&whPK4{mH?c#w&34yEqkkJPkgW0 z+na)bWqfI)C;r2t--*7~bEaLt9KeZ%~&7fCaIPK2}5> z%l$ADFd6~|I}s7l^ZeI{Kfke=D;ddH4*O{|1l!SSQTK%FN*?&uu#}5gR$*@zg{)Il zt%(1*Z&RX4*&EOoBt$qP&P?aBU)(BCM^!C0{6#6v_U9Ct{aRnrYgh50Ah18{U>>8s zu*Jl9jrQYlLxREIlbgf3R)^qyhl!3AG+zDbwfVIUVtl8SJAZZoAz@G}*}f+v#3dmn zs(-|F<>-V0Rz27mOA90ZJ-5=$Y%SC~$YV{-?$zF}UHh?dPg6^q+AfiCMBpZ+s^*l#tmqnwn zLK1QB%pcnp!{7bzp`Me2D_m!)h_KZ{h%8GvAV0RgQvd95<*B=g~~uGNB&H3-DP zN3SnGe%yO$5ZpL|W~-{JP57v(y_Kh9(i?IM3JT0xLGOTtiCOKkV+hubN89sw=%`G- z$GG&0wC(u!si}MLU7ej-x|WeS047?+mn73Ze)B^gB4E}|#Rrd%4h_#1I;b0=gl;{x z&DSh{f>Zp+6_4Zbjg0Ff{3B!}oIm7jj~%3{Qi%wKKVP!-Yl?fltia{HaHFxN?fxnQ z;*|;>Y7Z24L3cR)wils4JTEJ`2DbAM*8^*s0Py-QxuA7M|~@HzMsOBfFlgt3dBb#$BGD7ro>LmRV_%5p2=VaeYkifIgwGHmLXIXO&`Kx**t* z|Awm%yu%qFHv9e-l^+4K^cP299P~eN`NWm5Z4I>+XtwlSrg8Ic-@a{p1iHecD}wHa z)+0RkZ7M2l6!^z6+*Z=8!RZ<$rlR#$0O|#>qYuJugTxq)aY(#|o_#y@uy~0P z7Z^nW)f}*b$J!4aK5sz$f)0#`dR-pRQywKL$X*udMsj!{Q}i)V*R#ZsiL)cRmE+PS z1|tap0T_W|cEDQ+7CffiX~^=_NPkW=Vi=R|LZ{#N#Ml*D{o#h-PFD@wDs^>mw$I!G zmPV5GGbRoK(K7(!f-Y}D$UV|aTq=P-I+=4X=};=#kMAdk#WD!YGz;`#Q7MiC?iDzz zhTF1nLSRT@cq^T*f>+zlZhuNwjL+hm2`2k%RL`iIrs$QjS(x#z=pt{-tBX&==IQ2R zAo2mT)LvoqxY1Ip4G5w0^}UK340@j*0HA-!S0m&+oSLP^FE9 z$up$s#H2re<^fstOioiS32)4jo_M2!a~33VLZ#Ejh06@k)jgg=a=QPpfMDVDCzg?& zj!bceN$7=U$AAOkzA`yd{O-t+DQ6G{0G+=h`1KxlBns6+>=9@!J+0+zLn5Q-5<}a0-WHo3DWj}>DUgc>_iS(D;Akz~vAtt3-m!?l@vC9hS0hYJM!?I%?jYaDDCo8Md#&64HqVCN z9t_|-)oS>P0Z(R*5~hZm@d-o+&Fz@5bIYryxh}!1s&HK6gH&7e3IL03Y-~79`#*2= zPs^=EgC6Y=VC#%+HavYgLxt3iujtuZSR_0le%+y=p^909#sG8S8pMQjw^Kt_3n)l* zA*b&+p?n zxNTx+6QRxmxlKr@mV_@bQnChC<>-yX@G~NlMq2)M0yMj%DB&fQnylMZx-w4WQ;hmJ z=L10kCwB%48s{(+|ACng>2I|kEOi>X8S=%ivv$j`ciN&%EIvz;%{&UOUiM`YAl)ruy`E49dj&MCb#IomyNJ{OJbi`5Ez;_7}}VMX}oix97jLMkt_kZT4^ z<3;yb;b+ej*l53ApsJm8Q!U2dh!UFp$+p-bsa2hjJ>x(pu2Hbgy_V3zW8NjmS+XZM zwJZ2d!zh55#!ne`&VN4ZzWTN}qK`gNP4DaTS-Vo(mVVl+-341Dt#`cwn;(tD{kx0Sxve0F4;#h>lh-YF74)o=B#T*z&WP$ zvvA$mnZu=Ma;|k>BEEmEwP!!mzga3wscG3(`*EBKOXK^XN|krJ?{-UOnT$&RZpOMJ zT3_DbG{ybDo3X$#&MPCK*3Y!Q(ILN$S|mq8AuBLPl!bcd5~{^?%e&os-Rnh6i=`UO z>$l|f2w$lL?2x@kt}EQ0X5^~0Yvm@;A2#p4Y>B!7uXq~!t(hkHe%M{!SHY`9oC}A$ z!OkT%A)#yTemNteWn!vYI5YC3?Dfypvuw_s{)t$5leAzh^t2VaomPh@+gi;pWX>$^ zdYc-A-WiFB4~)I9#(dFKmy;|#KGfUWE1k#}eWq67xbektFT1}V_x?c&MDA@2^LQxRZ0V$F3PAxtWBkl&kl#CDA)pbtuj|*9JbndnOf*nU*d7U zf!Xn>>xXp=v?KmhI_PyzOmpW?rPs^^`c%9e3J}xZvoYi9qm~=ru&UisZyV^k`cCe9 z+I8_UsdQtDouql}2MWc6oh`~eQ5EiXQYEmFW%ia4gPxU3J7ZuVaX+3YtKG8!Q%*g- zbwN3V<;CE`N=Hkt>z50E6z{EaaLlzcb_z?oTi?|We$xHLw&Es{OmmK#ZlW*&_E_PB zEGkRY{$#h=P1G+nHPOC}c1<59`(46wX)4}I1&I5KL&tD4%>1!V+GB9+A&xlU_bzFN~w zHg^_(_q8xGoi*cpyo){p-EYO>x}{-_w&!e z`cLHVrGl}zA z-sQn2yVgZ*7hrdogR4Sf@&@ubI+AfM;^K)GHnzS5*az~PT<;RbPPWsAe#O?W*o*BtQ(@DIWW4T~QkDCTu@>Va1MN`{BF%m{=;RD- zV=i6Bvs>&Kt8utd@(z!&-)m|rxyf&`+{0{rHQGozF}ZnIsB5xZ|6v}xU3ZIlqg_ju zT7*z{)^8!koR<+b=oX){%|o}}`g>^irf7yQ?SE&`N%%qrLGjaRpD6JaqDaEM6t8=z z4Hcn!+>pUIu{m1Pnds{4)L5!ywj4@b(SI&UwWqujL$tOqM5lf6awe4d0ruBBY`CUP zZXggktjb!%6rVYeDL;2ervGN}Gj`3R@*J;nC*FO0wu4nCjvTjMOJO;D}o&q1Ta@?qvhi`ImPzpZ)qfsnmGlkM2Af?5IlsYfkXYt3E)hI0fd(C<@{ zG+1IYc>Cs!;Q8;f>$A0&`tEphIB2Nkh^?gyHic% zhDMHZWmiSq$g}31>20VM8o)ID^=o3#y=8D9#i{i3gjrX9T$zQYghly=|M^QSf?^_{ z+dgbZdEOc8F*mcjPF`<3>T@tt5#AUSvKvj3f36NoZl-0f5(GK}h9SJp$j7lkvS z0=IT*!djZKisVks#G$f?NT+&9ET>g;WQ=l=Bk7RBQRWCS{(?1+da=1uwmp&SH`|Ep zerfMXqQ3OzmKHftrvY*Xot()>>$UE!Rv)fk*Wj{}&r+Na6jaI?c=|qAXYZs9I=17L zj&VGmx!Pww*lS0hEPmWtt)5QrGLev)tOysApY2^&1ADo@9rk6W(50;}I<(LbK_4SzfWf z=Q6*JNE)CpG^aQ9UpEP{X4tZgmD~w8!b!Q10gykt->iPx4&4inK$5(H4jgu0u=u{XL4u@ z80;ndRf{R5)1o(tB4{l>F>hG?)KH65bNxM3+;8tYt<5Fdb3xcFz7=fbBC|AX|E!`n zcnDu!t@u7_Mj_3OWZ~ydsd7ZJ8nOnr1Nggndr9b(!84>SOV#31TBpvP@lFlJWKs1u zJ(2lLnH)JCBzqiu{kiH)rkDF4_aCVqPM4*sif7+dO;zAyRo|n23-3{Ym~a$dP0w4; z9LnV>mqL$2^oCcB&Fpaxm9Lh}cok7XWQ^lQU_SJv~ClGl`*ZRU8s<|2-9E+30U`&#<=X#RtiNW6!m+#_6k8 zY43EN@(Rm6_T2xuc7|U`&9_q2yK(qlSx1pcw&=yVVZ7w-FY@y!_Xix_L?*Pk+{`-e z@7bDlN}X!7vh3QwTF|4ZkSg2Q+pG7d{odIwDOICR*32vRS>3f~V`uo2yY&A)7=AEzQu#1Vh%?+9TK86}p@O zzV#et@c@#&7X`tXXpQNCEAwW3>Gx=&XqA?OIf;w;#uMEf3)J^cgri6Zh#z{6{R&N~ znTz*~W8dj*x*=16$?NCfgRa~pGEC*Wc5%4FH@R!Y%F`IhYBzZ=O{j%9m9~NYS7yLy z;_5nkcGqD?x2-E3pUC%#)5(YJO6UVizj1W(kDuFYpye@UBvpLdp|9j`&;FHV)UrYp zoVB(nUw&eGnxbCW0$BSV$ z@Ad~-B}?&i7F}b!SqFnOYB(+eIhYD`RXld7@wW)IDDd#spR43mMJg;R@dUha_E#~? z#0mUR6{)nSly%wJEN=XIhx6WlSU|$uPXd)9$w=jUF{o0WX^oU@PR`yE(dN?1$J@KP zd?NKGWf_$wQgg)oD}^i}n9cwQ!s54S5AIVynfTGY3;HM%(_Cm}W6NZckX6)5Lyt%%NU%wwh=Q=gsu`w1tvNm&+6 z!R8EdpQ@G?)ez%(p=O%uE$eynjm4mBO2*^%72Y%g8i5sxcqKZ#V-X((g&V>$tXRzp z^VWpyRack>wo;q2jiak{@r;=i(^FjIe}67l6??f>gH``>3%9r!Q<6ME$)?O;rTy~X zy_XDtQssBrC0T{j!fzweZY))PO!15ReKXCD(S@${J!Ruc+}o#Wk$rwui<8~90ctFy zLBok%8QGWd6w6XFsm9NgDB=|)ctt8I19nX7=oeop#fXMykoGRc^;+2Cb{Uw8h|SF< z^!`vT@hEg@mnV+CpFKF( zZmwR>Hxj*1KkGQDpw%H+$@l0nop+y2K2N=&V8KVb&Ij>+t*1pZ`#%%5BUv&#t5Y{? zF2e*ZeTTp(NH6a17pQb(vkX=!%0?`aNNYFRsB20QBw+s;&{Ndj2#n2T+aSSt`(7oN ztcK>~e6()VKX0?mUchXTyG;y!|_e(quODt$) zk8801m6>x-GXH?o0A^`C`X8A3)n8~Gfv~)E>0h}y_75F`)f4rJ(GS9D*IG@zkgZFvy%@-Fe^9pCTcRh@OUjR4SC13;q-KZMZyf1z?8N`w*i zgo+Gei2yP6SUv9>P~^Zk3K9%`vB{-nWV&Oy3!Q+!8<1>f$pGoEw3OQ+#R%v2?KvX>0fC+Qc5xgiN4W?w^1aJM#Ki8?&<0M&JW^H~})=+Iv$77+g%1;+hKHR28$7g(6c>pvX$I5th zNRl+B>039Ly8aj0N0fOl59ENT#{X;1X~<O(JJFohr7mq}PPwc#N_ zh7d!>CT%5`*`PT9mcWr~0zv8WP=U;N&Ds9w(axd*w~&yKY3dXov;%e)yW6`B)eeC! z2dAHq_60B*5IpJwz6%iSy;4~}Fil|>6_iX0SC2LlejKmu2}m$LoIkk6d97Xi9Cz4T zR$4%nr;h+mCfH*ooo>R30bmR<%b$RQ*!t4-VBCqw`3Zm)MW)r=bxQlc8M12kySKyB z+hPHW2pCP*khXZKq%OSD9jM`Xvv-yV`QQ-F96__Rk|rE6NRM`Y{wnY&QhbuiQ)<{5 z14oj-Dw$nkI*_dC3J8v?^XZ`tePr+VtIQmG=EGgs&D1h?E z;1;$FJTI*^;CH2>XRl1RhS8A6IRT9dckP?y__FEJNC|_*H_5c*RB^R%ms}Va9{a;a zyUUqAr&C465@3y&(m)p`5ImyQ;XjZfjyw&kJvW ztf6HP^_nKsjiD~iSBPBk`z<>ryOav|8x&^~U&wn5ff7NfejaOc?a#$Bhe8mqJRg{7 z{c4L~SfY^nvE~*>xR_~(UI#+hF!I~Hh{T6ZK~LDEkQz!3Ex2^?yoUC6|6b-!(cyqz z{e_Gm65g-6^>4(jNEN>5iK=fylIjx&!*bD#zG`^vATU({%$N31&PJ8&ES3mWv1pi4 zCTGBuUQj{Z^Y=DgNZ;kFn0Iyy9;@$Xj_+lb2-V1mjT)Ay7jekl`KjLb3Kg^Dq2ZQV7dfxi<{2)eh0V^!29tCQ+49*%ijwmq0a^damPM(R ztNI0$!pKPgb~4)vjP0O#{Gk05^vfSVexy|gr@~-mDH*9xEYr{iIfi`dt~lN>mm?t1 z0a*asl56c|+wu-{@r4YX1j_b7B`tCDT}PnZLPscy7Z_}1`aLx1z1Nnr+Yiw5RSd#M zUm|U=HqSxSYdVxK(lzCabHoV@}-=)!3C&jBk>IBgeWY?>seC9jw zl|BrqMhp%lXZ>ujb;Su=QfY!TAvJIH1m!uV*k0;YD6J*|A|Q-JeV=oO0N0ryS02U=Ze$yec#lWrRVDvL0T z7248pp-(afW;v1{!_gq-NlK>25pq-a>v4RL$+ow)LUNvhT@LD%D?g%E+g4dCSL#a? zNFs=r<`tCoA?+J_g60qrHJhuipcSKi9?7I3oxrbF?dsry;1yLHNkm1*{hYu-#S~}D zqx3dfoL24Yy}Teceo})F4QQ&(KpU12G8DspRO9JVFim(aR%I&+w`T?Jh77pU-B96P z|CD!)X=%CfDqfNfR?KIy;C=nTQKR98V7fQ{OUrxDtL>yUa&QD9PW7EcpW2L@30SBc z_&YM7Y$k)zS*Dy3;si8$!NIIeIGsIZHLBeHbZS1F4q+tN7)f7&dIKM6SVp%k z$GQ&>2nm_LVj6PD%9S*URu-YN2cItJuimybH8q8!!92qD`#+3*by$>LyDtWyq)JJv zC>_!b(nvFOBS@FDw1R*%O1H$&&CoG4NO!}~NO#v+_iS0uF!XzRZ!&|_ju~EEHnZgENcEzTGWc;qM#Oa(a@F1t7`8$`NwCx#DX1o@k@%HX;_^=Cowmh3T69 z{Vz5Bg%}+hn_STIDw0O&P6`jmM!>S&Hsvv!2>q}F91xvaA$a&+jY^Z_eF3)BX~0qn_+G@YU%buUXh7kVy(694dVIKCJH;eIG9l^ksE&b~xn51|?D?(Rk)yuec% zDVVe0gyWMkvOOJEN3i5m8>HLmV-dp{pin=>&ZOe3Gkjo`_%kA6-KfiiMrl54SNXEH zjwgPl2}wH#0=>Q7^)fGa1p|iS%If(twC6B78)kSP#!nQIe{h@&3^y2xO6}(D5EQ(x z;z2`ZVjf;vxP2hd)$hkCo*g*WF3EMbF@LfL%v6+8#H3|((eHePkCkMUVW`0EPTzkR zwqXw%Qh0OZ0YaKI zF4T&2uR)N*0@n^0Y~K%=%uHiGbS8aX%H)hdRmY__sm!ITYsz&RhHa0&EI;q!zOjGb!ulM+0AJ^lnlWhzpmbB zXx^LbsLUaO&xNHRY0=q(s$Js%I@`@vG$^Zbii#T1vb3^; zv{i>2t=6By<&(0~vnp}=j5NUcY~G-`Ttp+e7{M)}*Hc(MN8Y36rvjVh>Oy)~*wWrU zV^lSB)33<*)RTeak~TqIoB>T72rLo{_23PTo2qEWcg(v#s2#xqaQWkyO15$r>;cj? z{4oeif%m_|0a!M4BqblU>34m*dva&gr+$z6OT}y4-vbOE2^MObT6`Y3*=A;DxJ<0h z>L7CgM1x6^8uKc0$CClWaoj2kQHXNPT&*)xl!yhFjMG;A`js=7Zy`_!G~$Xal;Kk+BQWypq%T=LfTqie}DXm z=XFyfjsZFzZRGV_LrW#4oMqSYTh)S7!YW!@h;+67=HP;b(O0SW_B;WU>x)Z%9O;%% zsa9}&JWvYcl8*~bS@XI%$Cb?^Dz228B710+<2P&)u!}|-PLhuo$$%8nZ>hefpXK@( z?u8ZMc>{Xlq0``NILrUm6xYGJlyg0IMEe?6bO~P-ofz@5gkuIQvVfg9&7%#lbx4gk zKZC-1=mNU}j?PnZzk{vTVb+#2284I;`PP6+;dl9rmlzEXh#HA}P2*joN$obheL{8d zSm5ZSt`yuF)bX9EFB20J{&RNgsbFR-8CUYF5 z)FDJivZEnKNoCY}7H2e}HYs`;2y`r7X^=Ir4aaJmfnnhw7 z=e%KI`RO7YCI#;?bRPGy@T$R-v^Pk8f|sHO4CYdqh{R~(2gRn$#)Yg}MPwOF<1K)! z(jI;<2)lG6V_s@p4EB+xzD8RQ2o~X4G^llX<)SLlMSX)}XU+Bprd5*8*sL^ONBzXBI?wUbBUq2bL;ZV#WjDfrIRQ%Od0 zLb>}y1~!+ac@XsNA9M=q-gCR4CJ}#Zl7oQFg(D4nFU`H6%@0Ka*W#fUr9fG1d<*;H zqWM)w2qBkP=XrRNGi+QqTWmDf565Lv{5ggG&6nMD;``T_u!-XG4;@iIS1hJ1g#%w` zA}$*3JE9;N^`EA2xdx!fJ55yHBkqq>ht)6d zUUnDqYgTy3N6AXsJtEw$y^PMGjJ5i~wk;D}jel~79X3W_e?1{>i25o~h7gD-N%@-f z>FCb_w!jvHJTZnJh=&=Lu+bBQw|;wQo}*AQlq;wz`pw^dMkzEgqj?t=XK(H243_{eEg_f z2ywg|V<1aT2DA*f=N7f^2Xm8Tsm%(Tq0_+>FLR(DY5vIODfof!0aNwz!-a$M45?9{ zo|9{VxqC)*ZaUSUE3I3KODbf|#oo0Mk@0fq!O&?o)n=YRFWJo1qjAJ)%Zh0{N-#Q# z>kcE8#c@erE$L@OC~-%tTzUM=RZsN`d>uy?AAK=_TAnXLUi|&SkQlv@tHoShFQ7pL z*+aF4mt(o3xupDdvG+fX=8n=Jfd>2KqA?c~K?K|eE(LQLyA4(|FpZocBlkKFT9q8v z^P#qqbiu62QS)Q}dci`Cyoy<41kCpO02;)~1lK$!O5n(k8Qx8gq}IrlM}P3%Zt8;h*08$*w~2@rL{_4r&0|oID=-D=PVJ*jWli~uEdSh zpEIH7S#MQyq4r$hSh~9QqPtjON*mfw^9ncK-3cPeBjd@`d>4Ll=-Gi9Mw6{j;)-2p zPKscah-k;EG3|XzjTg`8;gQg=o<3?@%Q2TEutRCcDfu{jfhI#GCl7~YamFk`xh_RW zXQqYX-N&so{SvKFl5{(3Kip*^7{;GL_$O1(-lRuM8HzDX$uNJs6EbFIz0}2;gz+Kx z<(8fD!^_Fm=25*v87bHe+xf-LaSmEvI1Rsn-bylfl=mV(C$DjxQ$Bvx;WWxVrJL$3zi1W!yC%+4wzdJlB9{tA8sn-h8;y54XM6bg5BT zXkKcU&jBVV&^H?Bw<>^39ARJ?7@zcB;;IF`q`!c7#R9>(o0L z4y>`Gg!LkFCZkVLI9WdjUe9UG!BqCOeq<$70@5PD`F1BSZyfV+*!Rxj9EaTde0e5% z%GoVo%kBi!Dwr6;g0v^E>{Y5+0EDtZ`a+Dt_o@t_T|i4YZsr)ns25n}65**82*%x% z7_2PyAtg@cL{MdWX^8?v>ybD#N#+8I4VM84Z z>@e?^H=|xn_VBt^xta6TgrZ_N<1@FT&eV3!;O1)|0%Vy4t! z@+S?#i-3fL&tj*J$5D?pp|^g{Yj?2?eYbAf>>|G!<7!;p4~H~&33ZXzDUa~U$BF9g z?>|okig_EPdWG3Hajz`K2}4Jzx%iiQbcU`!KWAp>m06KWV$t(coadTbkW|XmDEE8( z2!e{T9!icSSa%z!BZ(&(^>vNLTrVYks`E0B;;rTJKeM}ui z;8<3D|C76$Stzoihk`@~Fy{G%heE$brfOtCUy9YuAQ4KE>2K1X*wxn<5J45Hl^?A~ zz;JaOa|?w+6+?TkwifjWTrSVtnO80JOh#jjCI`X?=%ur@p&MN*-Jv7`8RNvu(GY0& zOyKue7PD7YkdnnWfBskl1)dtc9*=D^Cm@H~^=h>nUCcMUH~E~E8)9)}OZf6qw>qRy<+!9&FP-LCK4*Ky!7WZklALO*oPbkh z?RN>F611$l$=?(7w|lYYcvLo=qsLT0_RGvR4f<9-6-ugBRfYM-Z20eT6sgzrcZ|CP znJH4M((avLmgt>2uE|T_Gprcxak#j;x-*;hm+FN^Qj?mNDlV|~x+GC;}ql_Smtdx6|>eQLWNlxa$DT+U0O)f?_8m2E76>2AYj2ICcFxb4by@~%Hm zn%BSaA+v#tam&e5#8e73AzA7%8;S~IC&`O05|ml6x;V3U#Wr3_9wg~V=EdtR$x=bn zOHTV0d7-X4fVtt6-*58L!*4`6I9&XRdO@hHMW5)5%;LmVpX|IGc@U51+l{rTSyq`b z{^=qq+5A%7rK?#%#`RAl((k)s-x^z;ccqFn?`bn`u?UemD=F>l6q=d)}aI$&Ou{Ni?y!Lore*L>DKBy>{Wh-(y&j;AS?RR zREa-rwkJCOrcNsec{)Tp*QNFD154cQoNUST!>Eyh=N;6+6`umXqxJnd43(D*uACMP z^d`wl_1!}YYG-9$wa$N`@RB_xaGdZ8vb1DCkDs6la`biL@fYi=_Hn8hE7}^mZW;_o z@llZGA0s0px3#wN4br>TCxiZhl_+F#x6ZJ?LHv00E%zOXJemQ8@;CVO8l%H)`7g_+ ztt!sWs(KiHzkiRhOsV{Jga%!KKSC-JZ7#?#(p>CUWd&`^BdIz6QbIaf8iPR*DLpK) z7ru;L`mZQl?aZZ&__EZPu@YLQKQOykASVwGYLjIBw5e$+C4q|8)5@&u1;CT!eh>W8 z=Y|GGzuWwY^7s&CEyNrz)WHkeH%?KKB7|F2$evp?kNfwuNK{Nf^pPmu3FT?Pi=v2>k={%9lH{CeBYlSOC-)_+T`=sNeD%PYG6{f43Eno_0* zIT?M{SM^QxiPXJm(H)agWyNSGH*cP0VDVY}GRhTA=;X@4-{_;wnXP^jQbs_9EH=n^ z3a?a26A3R(w2#AJO4{yndpm?u9C>jQL_BS9M~QvWy2QT!Ht%@3-rM&`^d*1tJ*3ZA z>Lxn-p1uPvH?$o$=^cpM86(Emr89`@9G{KB$3aFb*(YHyt1P3DrHC7E4r@%653bf& zT*S}y*0}EM3Af^slHX;CR8PXN8-WqsYB&?~@M^g~1rB@&AXX62DIkBKhxHP~azr!g zxbDnwqs&L`85G^`-M=vQsS{NHWiioRwu!AfYqHqc2zsUB=}YuDI#wHxNqY&rK0WxC+Lq9-TzA%Hia*SDB3nZR zmRT`}nmE_tv>9x>BsyqrprYw9EY#IJ`t7I{U@sa>JDJwWyPhc(c9Etr*3L~Ebw`+& z)M(234{NtCo&q^x8oSSwW5~snHHk`0ei6N@78|8v?Qxf<%gU5y(kPzR;J4$M*P-H; z|HcBGhEvf0lD1#gGJxhjQVz(|t=>KdgLIBsumxZ9f076Qk?(+^00`G0tg_M>#I(>V zXTN(h13ViyAer6!1qe~Vc>ea6R1~dvx-Ag61N^GL^xNim(Qq(sF|gSrR@aLLJOd!g zFOxgw1k6YRV|wpqeuF5J)YQ~FDoR6X7Aapb+0mS=u zyrXsqU>yPzqd=~z8zAy7HvkKEwipDgDUkL%I{`4aEL#I>)yU>CXj=ih;9zHW@a-{< zsW4fTEdL$J42Yp=%L>5CGS3%MUko{SHQHTx5OxT%GI1dV;TvugJ&>KwJZw0xn@cFL z;Z9u0p!22`4&3)VAL5jG@-rQn_F6PVA3j!K?F>Y(Cr~;x6h$9q9MsuE=8aou#L)Z0RP27 z;qefkx~eapz1zHeXR^#6as+~b-mA=j+!8=}j1=h!9s)xY0OKOv%!su`N^^({A|d-A z1LAJ?iVg8snx)B;{y0NyZ|`reMr?jtt_^NUzLJtZpTXaZS0y+?*w!4L#eb)RH+EFqLTDj=Vk2 zEH}-C4Ug?4R#>(kRGwZbT3Ujty_5~Yf5%+{ zX#YcLNLSt6d6Y!%OoM0Q8Q9Ev_&S^ZYUHq)t|iJ-p0^ubco+B58&r?|w%e%TWrlvW z^AwC?hmv3HJ=b9jAFjqQVizWHM%J+jLkYlOh}!LIO#lYkbTRjzN&-FBkn1(SryA=7>12N^<3Vx z%qA3`-P|Kd@S_mgqt#wzv|(5wrILFukE2uT)vU~fi{@WHMkDLSRQaY)j76sR1`xe~ zE#4g@C!w0Wmy~=Cj8=GVvXl{!9FS+hXS*m0fee92?w_Lxi?pks z;o+5o>jY-K^UF)6UKe1G>QPFK6tEbh*E9qC{d1%&5RuKa{=~EbtnbO)ZC-ba#nvpQ z@B@KVKPNpq6fX(khr0>=cGk5HwG?X_b#*qn9*3#x3b0{Nid zBA){XOO;C?!(s+Y{?CS$PcPY{hh0FVK&DM%>m#QWp}Yu_dO8G}I-#mYO*5Zg;`nRDen1JYuQDRdVOBTg`g=ybKUbi_0eGzW( zZ51<7-cTuzjZ|Hv@h(#<-S;lIf%pc+c-rYx;MRQ{j8`@WSKI)EX;R3GV9hT7lOM&)IPSFC^a8C$$#!ggDKIrlPXsx2E4W&bB z90n>YzJ6Auy|+i`&C5h2VB_4QH6TR#LeOen4#2QS)f)s4@$|nW8Zevux`>obJ#PkA z`WSSEF9@DJ1kq+9P)I5kMmXW9C(ZQ`)1wHB7yk(3_~ovibPeCYuW+EMcwerU0r(*B`ZBX&#^`MX zw-0T2j@0RgZz+ay6B0dYQY?;g(@VMk`tO+{kFEx+?HavtQW#gn4C(#)PrlsFQlk8o z`^~TaCpGlBbo#~LLcLK0Koh0(-AG?>u6YKSkR!T(-T!<2d-;FLZU$0CcpW!dZL{Cr zlYA(2m&Fy!Y$&Su@eU;W`aR`p^VoJHBpPZbIp#O+4Hi&AwkU@Q5Bw zWaMsp`i@uVcQ>f9ObE%3zff6Q$57=>gvxsR3iDfqvhcdkp>}jzpzf`KPP)+vPlVX%KNiGCD_z;kF}D14b2hn z(nA*wQbvwx2ukPccY&i_3&VA@@#evtQ#mAOpp*nf^Z`;MoejwQ#}6_BdZjIjn~|YG zL!l?7AjH6qi==v^rYU?9q&i9>+>STe>Jp!PSr7ww0u2pK&mleuN!sW}DH|A45HFJ{ z9m6yTqjMDD2pFm|VHRLe0Q{#%Oq0BFZ+{^W-pW5u6K1C%1m111bx>I zc390EHKC}zuR6UZZ*^%lwSAYqs&J`wb%v0%7)@~qO|kfGbPk!CzcvGp=01?^L4foJ zgflqrBxYr0$z`P{-wky|vLlp&1N}u*mfjFXFcQXXeX+Oi1vZH`OnslX(xx|w|FCCW z>HL7;Ige%QC|!&e%yM)+>BKz5_`lnm=%TtlqF0%-cO(_2}kyBP#*$)yrjj!JNvYh{SQYxo^ zi+pW&rX(p^#_QW^UtG1vCce3)Y#48yh=rib!Pyac=11LCy7vM{xeLldQsjZ@=Bm)u z5m&+fQVKn6&w}t-gW8Inqsg4w5e_kENv{i2d9OL9h3abMkl|tEV>-dy5}U~Zm{tI4 zgLaG^JlKnNa2 zBW$}s9|zQrEbv4i&>DEi5Uz_XJJ4+A%@J0CDuHRIym~|@Ul&aH{N8h2*q~uUrdRdy z`pXBG(5x)k#H*IlH*n9x(U=uFeaf|6aOPVK5fvH;QD~|8fQzB zfZ@ZzDbB^w_N`op9BA_6PnQyxGcu&2@{ZOy=Qo-4MCYo9tISvvW^ewlL|wx_hB3in zuapgTb1agp-+kND)GsT|XPp#o!;~OD{V|9K53jM!>U;7EWm=>TUyZ-*`?;R_IH8b% zPt(zrIIP(-0j}5NmK~4j(%qR57Bbt6YKq3j=;+$A5P}meMhxM-8Cq|?Fq9U*RzAFL z$6T%B%^J&&_nBJ;f&X&}>J=)-=!$CewaAoVHI~yGbjDZ44bd5;8a@x1_={4fwgLjC zG1{(bLU#=>hn$?wzb@(7Kz~}VtQV|Axe{PVO4cWscK#_YUh>84z&FwKjXvFfbckuC zVz4xVS9ENyXrZ0(+X7WBN2_3Cq=H!D%5}SW)Cu8GbY^uJqO<0od*2}+MQs#F7`1)9 z|9~w-B7{in+#Zz9cGS?5k&zK*!*(#cNMR~Np9GL-iigbGaf)Xof}ZX`6-xc8FOJm& zBnklT5`KL`?BPzUnD)@O{|-_(%Lo9Xad1S`g4G3Pln8v9Z(K|9k}JEDqYG3FYMNeT zrFf>TuvtxwlNo!f(br} zPTc@K0#bIZuI;0#b*KlBzaLF;TF*!hk5mJk@KGZ-mHkCjJ&0VnTgnK0hB~M1T)w2( zScTd?3jl}YdB*h3&emIWSb(tsd5FC~3nFPRdd@!3woP3jH=}*?9c&WLMNvwDjljB8 za>d2xK(OTQo>NB)D)vlZ)o(5dRdny?#5fT~j#?a6S0G2B=6J(-d(5y$4WZy(fcUjA z)=L~l&MVb_bJ2bK#|^NXR_gEy5E{rvdKVd}N{ZIVR{tM{J|aGcs>(2V@KcDu@4ueY z+lX)1IL^LAlF+NKw{F@cQY~1GRP1PW^mJD^E=FARo!PHok!Fp#{;_bft}-5B;us`z zrf_X`OV3uAncp_n)bkK_HCUiDVlWPx8~0(_j>1ie6@-ZfL`TWn@cwk*WTw5TQQLoq z)>!K~P124Wbwt}5RA;}%wh8=>M;Dd3<~PS~<@qB8y29?lt);_WMGm!JhX-9PU^c(@ zM?11+^?cJ{YUZvyR(pZFp4??CkJ1-;JWmzVDEZ~{AcXJNg@2Vw>aWATjX;j+L<0hn zvFfKZ9vXu($8HWlq043VG$htjumH;JP^l&+=}G`fvD?fg8j#6?p+oM(Ep^kKS955Mx3YCURY3U9Mic7rDah$~`FJHpdLPK?IXQ1l| z3dXeAD%MiIVw7-U`SY6TBLLbHHS;Nrbx#lR%eBOVjuM~LAf|v?ICR~ zFDq@7!K#7Wv@owOS1?G8TUYl6XhwR8Ip12pxN+JFJ>Pr~C7@fU+i!M_CPiQ(CnwAN zX-WYK?`ox0?ow`Ae&QQB@ftG^O%dhHv_3nmU9Sjc&gD0IxEVci1jE$3aoX(aZ9bMh zJx7@EW)*R>8gafc-&p;sn%cqLRicbw4I1zcQt^XugIyH@!t?3l`_ zps$rKdW+E5_B65Aj(bY;g()Ocp7)qhf?CY*$Bk`S>HTyA!sJn87g`MgX4;+pH(J|7 zgXU#H!7q}eKsyl58{2E#)z0sFG*PQ|ktgxh9%w`P-A<17_I|}qWyzCQ6MImbgK=7T za%LAGK<4P__rMan3!}szke&BADh-V+Mw%O}ZYT9_9|t;KqZNX)lvcZH53B)T5Xe=| z5#B!lU*rMTN7`lMNz3tOGyGhn+Drfz3f+s1mKU=eKcGrYx^|ugkEtiI`}J|_%BE3lfa2}TLNZDdCu6p z1HR5XpJ3EneEFt3kEP@)7N?D07Bq9N%F_xl2X}DwIK;P65Qlnk^Yiho-Lg@=e3?vJ z6MX3Yj9f2;9vvIA7i4ND2=?FpHZN10js3~s$O{xfm1O)NYmxvct)S+|I2~che6}+N z6i}Az#we(UhVoRqH6kK&|f^Ly|SSS(J3oc-Py$pG!n3Abj^{wtttA*ELdMD(R#k= z+-%dP6XJS!Xl)S!$L&&6_|7gtUiHHWZ$E2ynSm*ZtF?^e%}0hjJL{jH%{tagZN|77 zuIIPY#}gV5s~08G?lsvXdsV-H1hVce(CkN0Q7-~5|6-~K^$vuxZWD-KlOisZ79w9! z`|_*FkLP?0Gj|3Wi=*FbO^(&#QcAti2IfF@zTyv#muL|?P?U#~@k?pI0H8i%0_Z8A z1P2?#iBFK-Y|i)S|7)z!r`1bhMU(N9D&w95JJWcy*CQbf!@}Cz%KUu?fiKqhxE{tI z?aW?6>Co*l8og*4Ih?MlN+={e&M!AW3}edmQes>Tqi*NS*PGwOr=u|*7no-t92m~1 zD|gF?o)*jGb;Hzf6==LZw{#sv7p=$%a#lC%Dhx(z_*@QaR#m8@K(dJSM5%eNpDQMj zT;khgqDre-8I6mWwr|SSnpZAu~)Pch(eVdnk&9tKG7PdLnZs!Qz5F?rg_bhiY72qtJxiLLlD8xe&Gv zV|-~PB`PtaF>9h5tzxQSw)MqZg<9jyF3DW}M?AH$lnHLL<9U~K6J1TsA}uA%yrUI< zn(V{7Y_V{Q?`c)8$L3wn$cur&-;5o&3nh@;p2;M1A0X{#?utZvksWg zfZZX%o1Y;?Y7&?Uqy0HeI)E8xrC$^G;LbV46kSmOr~{M!Nmx(H*T4)Yh}Ff=<$PQgr)5nvNV=Z4q7t;5OTL4N;T6{)7TDk(*t)dRLgAC zUwPmr6UmAF_z7VYlaj8ob;-;5?P^~$Y09i|R7a^ySX_3x_+0B1RDnHDxhywww$enI z@WaW|h{cG=SL7K-`~ohQDN5s6Zc_$3?Vs*PsNqqIYvC&slkP_Sz)yg@BhG#5f|ve; zv}c{%$tJO8C|T{tlayHel&#uy~rz-qf95aM$PA!6*Ex?MZMmvHIdS}!@w0v$1gc^7?H^CUkJ3rf(FJAzO15$eioHzDBC=?1d*%hFT zlw9HhOOBqJ0ytSiRfv5KSj-^eA;G~JQXsIvJ+=|xLm8jN(zQ#m!=D>J7{C;!!c%-;+KMpp9P!NnH;PL71%z##E(%QPHMurS5wPT5G>9; zqTEFYDg|EpZh8$+~p6S@whlE7OtDpm$y{?iEQZ?ingz@N-K6WeJWW*Q0Ks!)6_-x3Y_vCixJ%XRB zK6Lm|nxvPki{FxZbYM%Qu^126yRrcTbvYVZw#1(o&vD6A6Et*vhq#k9`puC2qUFmq z=c?VKSw3Y(9l)N1WJlM`>LF(ti!u~Q)R3U$J&QW1N$ggJEN2StC5}WmTZ4Gz#_n%Y ztp^^iWs}Kqa#?OYWkaLlw=Vx@99ZH>sUA8JfJezf9gXKvckz}j*Ock0N>9mdu3H6N z4pFqph05Aj5 zmV{OY=~?bIznFH)SpL~5_+H*e*F7s?VkO8`*re1@V1zT-}iqk=G^H2 z<@ZAM3H^&w^dcV{i24`efOEJ9)OP|R}#yr;L(*i;Qj%i?EiQY_D247h2U=g ziv{@a_V{p-34q&S`0JHeHUV58*wYR4jBtvKNeG?pZtuL!87^|TNNa4Zv$VtN^Ui2f za+FJ2Ek*|BMUN=inCN19pTF0C3V`rqRdy-0D>`?!W#Q$^?n0fHu>>pneU$Uf8U&k1&-^%p;92e6|w0De~ zMmF1KGWeTZ_hNkM`4{aX#ih(P>Bx_Eb0TUPVA65j?aL)&YI!uQ!4EiM{%o?$km3NW zyt1~w-`l}wvFCAU7o54KJnF-wShduFJxXaRyw7XJULa?yJ0 zP&J20V7`S;@uu_FiZ2^7dtX0t`r)!+^?5QN&J$vg>3>@zvYb*Y5{dPSuY1m(EV=XQ zI84^{C~%o`0bVzJcdf|}K{&&s0?o=KHpd#if^lNc#9O!xrOb#aOj<2JSLw9eE2-YP z{sOoejDWUCVF0jbrS6Wq`<_m!8H45smrB+pRng4IiYDwgMGs@8HHM(d=hpMif{_i7 zuNG=r(;mY6OS#HkXldR)srod0^awO>bzMY$X{zG^Ljt?b6VO<_eT9=v&sV;@vTjr) z!{lrvex7%rhTgIlynLw<)3BxzZAEltJA_GWq(A6sqmw3*zc$SeS73NE1X>VZI?k3r z_ZNEVABMY8>2cHAQy46ZQ6~%1$dwvDeApqCctRfiR;*p`>51YW_aqw83k|_-`GFwx z6GOD~N%Zq6v@Sieu>m)9T=L3SQ^lv=CXw?%8-rCT;Cc6`^yB%xYe9#q=5=^n>X@5v zObn*yt8T8!5?gD-<+gjZ8iZUvACuF?B|aq}7;li3jt4U!es6{ldEKnPjpxv!$Kb;- z!mbtT3VIwD;`*CT<~aHw!5FE}o;(nQ0i zy;{;?Cln@Cj{AjOx2~&^nePzyCyT+Y^Ud&xq*ju!RhO^It@ZjRs9y-1?t9f~cl5G4 z@2ob6U-}D)Bpe&{QQgETpBU_|$9-L?Zzg}>8;!djN+osHOujW`*gQ5{kQGcKw4-+^ z$cm9jX@)<{KJ;?VR(iRs=@STC%cYm2!Phs*Lf+Jw+>0*A-FBy_fx}oTynIrwW4%?- zw;S7jFOaib7Oy32GB|EhRQ9Z+tM^?O*KH-HHy+?hkxK8zNfdm?`iRRp!V~36y%O5X zTJ+O1pB|+I5JZivB7h%~NL2_XyJYV_Ou;0VT947hXSmYvq~+og%^PLr7RUVt9P{y= zZ{tX?{P{4$mPtO#$h;4YH=1)ze+OspSi81a!uETz!qB~roF&i9*M^V0JFthP)5Be+ zr&ul!4yx!t$U(SS4*ga*zJaVgmL^Bg(EF;sM-)63W!`0E-TN5bv(mh2tQ-i7Wex7_ z*tQX$_jD0EnunK>)KNtTJA=9Q!Dn;#Qa<&LjR#5Oa)MN+TD8HxcjTNZrk^(kIkl4^45(?4T4X)IC%N z!=d4jR`Nb7{ILlef+SMu3`3%IZ#TT0+lg5YJce!eIfGD=1GqW~E%D_OAWYZ6T+osJ zrjMFGsQPI{Lk*GFh-oO=J_I`}XyPBkKB-R8k5#*)P&wXx55w=k_wa40Y9YmQXKhW; zVE_qaC~(T1d3Jfb*dtsoXMx?Pj6|)#H3Z=1Aby&|YAnq#s}Bs~0JFAZ_gio`;yCph zKv-OV0@H0pL4t`~LKl|=J2nX)>=O_$(BugACV6cAS!kC{h@#bL@0iYlMCHaX>5%YV z93CAVbyz|IenJ62PX7FPTX-IO39HeQzAVb^Lf(MuIZyPJd0iFTMUJJMK2{o|o2yWn6QXcpyU7^3MRQSI ze%{uD`ZZ7gyy2jfxfkt)o0IezRW}*4w+Y`VN0LtEmEz!S!ty>WWqK$BGh$- zSA3J-+ug1`~8eg z09o0y;il~YonDRo!Ey8U0~KV3;PUEmDz7(;Fy{Ad^%jwwY)}f^%iSAmd&z3HuvGP9 z7JgFmrIWLiE%S6p?>WtZ%BibF|3%{a8Al$Y1mTYQr)+1!%{Pn5?8k|xg@45M7cpt1 zr6rivamYqlhi8gyu@{6t{eVH&m_uDT;q(}ZbELk(M(wEWK0 zw#TH?X6v1gD2zU@N80eJX^dACH)sZOTbms98;g|+OW*2OX^D~F9XSuc6e}jlulGLy zyT(n&E?sc@WojfEoHoj}FP{*Eq)nfWr?dzXMgn#y$1Y0X8b#sN_Tk_q0Dvkdw@1oU z3wUta!!AGWA$7F_SUwFKZV10Q8Tk5ulov*PdmQ^iFBmkr$9sFGV|6Qj#}){$qM)Yv zeeq3>FvM#^*9!nvB0Mg8$=7L;;W>&)-*+x)Uc7gD{{D4v`viPXUINyq`ZNbEL2lG!h5nE6Fv97iY(#z@nQTr zP?+_Y6b(^q`EL%cnOG4*F-8`&Tw-N8 z4(;vG{q*_Au|ds-+;Y-spbV{<%@5Dfn}-h?2^BO>Z{%Q6>&D;Jyk4IwF;bswF9K|ZBdP_Ahr}IRCXKQ(?70eG*6Av zJxT$>_)FhwgW#5arISEGHqj~{Exl;xcenM z3Fl|X^wdU-58;3~2q>BpcrN!_)-t-8QUwnPo=KF-UCkmtVY_ z(~yn{rBrCRcG;cSs|s3cmUm}{pU$@O_O@JD))t>dkRwRze$9R>_pln$w_BY?_ywQq zrI2Q+;qB4mc=dvm8?B!-JB|B-{dxi-f{2<_n>W7FmoC?-dhoNHQ+ch1#|J$rPg9wx z-mBe4`_Uanx^Y8QH7SbpX&!(aXL^C#a^_x#l@pDEp4U^a2SltaG6|g4`nrxk-Y>MQIsPVo&QURI zbMiVWZ%cgAPwA6#)Cz8XpW25|(-UhgH~jga3u(RQllXGJxKN-xHr>7p?X|et zoI3N_@%O5{lHY^4-)TCzcVVw5&M`D=y_n0R*db9|6u)04v25_AtNhNv3vQPnUCu-2 zA@)3Ljk=Cj7V4U?RuHQrv_Fr~(CNa@VJYnmJ8_UBQrzNuF#Q2vG{ zA?E}$tfqZG-#K=uGS-_YM=S8n9EGUaY6Kn{^o`g+iGb$ZK)&T#bX09@&-xGBN|^u% zlwW{GJZH@^{$o7gKzm<*4~0M25VUEY;qxkDvM696g|ujqzr)^M&mVnVkGk_(=k$@{ z8J=QmytS*jNAP2(wd(d@|E{oP20Z05u{Orhpa##g1IF4PtXVuv5Bl!52|ggW`w4%Vdzjwk=A7)$MEj(wdZnTh^%9Glv?L|cG>^-3JahR_wfc?mCc8zInb8pnL=mw< zp6GBglqy4P%8JsgXy7Il!N6N-cb-vEtXrQ)J%9p=N>D&R50aDQ3mRHM3LZrROIGNAv_7aeaEpcCK#Ei(DA6p*pP1a}L@L1}Dc?cyNja>Ys z(B+lytQDV9>$^cu1X2}ESF58{ z*h}wC=4x)ATCI~(#yY+jANgQ19+q5NpiybF_cN~*)ugO%zVG>o&nTyJ-Z{~jPsS>E zj>0yfk>9CJIGLIyv-~-$LDyRp)y6C>@?`Rc!CAfK$4A}9o6zaJ`RIjD&+um^e&sEq zY(UK9%Ja+A)RgAmCoxUU&K4<94HHMGP(Ssb{AO^M*W5IG7HVVUOzqnU`2MMv zNZw^8C1s(y$~f)SvB7ShvWt8j*g-@6MFQ$c!7zGna3A+~!l|z+73ZIa4LFZ*JGu#<a1I6vWA1fakz|u8FhYys2HE|5*tNpJdKNu9)zVnaEh<>CG741&2 zWwd6SRFVAVa)%o8$MA=Dmir;{b@>Ozc<*qe%YGdc*uEkfIe1#6l^bk+e}Uu23(=(6 zIE$ZbrW;GzAEfd_5QZbJ_E5KIwaD*|%^M_nM~@t4(IX_Ge+0xutOX*hV1);ad0G6K zS25~3R=#2tQn8wYcHZJDD=Lye-sRnWZ|}H!_Ael z5+m1D&z`A!e4#6wZa9OLtUMIf-ers^jrXsh^SC6!z4}8)jOZsG+j>^?$vu>T{|}Bf z0r=sN+zQixh4iToTk;ypdF@tE{>qQ&G!%^r`7yaik{YiGS?AA-dvK zqIy4?Bg;h%u7_DV4u#XxPfMh!P7(^n+g)0)hUQN~`NZmn? zTM@ODHC&$Vr^pvjkT3K{{=nip4<%`lcl>g%@*WOG*Afj<*NJP0BHnAcP43?*;M7v(hbbeaT(nV{25nJhJINzbszMPFO95(~$_Hs; zSW%)RP*n?Y90tO0SQiDWQzY@qmei9Eq@z8hAVtvFU{GCOrQNg$6!u1DK~hURs}A1f zXA4^_?{PghG92PP3U zD%$?~Jx;Sy)de=-**s4#EH725|wWjF@H1#ufP=!MweU)c2Mm^fdoggZBQcwm?LAgV9&_#kkF^G~4(aD#6WsBg4rF zu2k7-fispGbmFWcud?D(cnf0ioYzzqp^Tu6W6DFDnpBzclH71Pgmt&J^dEdQRzvK2 zgQ6I}6`{}1(p+>M&i7{tbzqaWx+}9Si2eS+W>S+ac)_J-vYbun_(aNy(EfdkXMS1? z0B>+OEPO_(f6U0VXOlVV(Wf_i?0*~GeSRTHJy+9r=>5!5F8e{=y`eu)3y|bed*Qlp zxumNxDuUuFwHL#0GMrxtw6g^?6GggM5PVZ2I!zqSvMlwCLuBYZ(f%5mTq|9-oqo_w zoJESceYEw1Vm+~sadjJk7&nC#s4Q*QtK>;5N5n6>XDo8d7rF{-v6;lK)N#{@)Tb)5 zK|JG5R#j0)LJ7g=Tg_xt2!jbpz0Eu~dP`x-v~?Ne>qwPjA!q5*N?QgZ`&bI)p=PQlIBq3Jy?+!b1`Rje z30tZnbYFeE<~0^IDv4*-x68=MVz7x2y&2=8UsI}Aw_%NKf3ij275aklA?zn#TzBbZ zqg`fR?5z#=?e^VonY}pwk}=uh!euczGW5?q*mJ#B4r*Tts9FdXzYVp$J(^kq*Fy-J zEpfhZT!KFq!)QE<+T{GTJN~{?R2~+EEhcQcwv;yN!bRba@=RrIvD&Wxq z*v8zEM>RpH=k!&CfCIDpCMLBibn0sP-4*RmuEkL))_*XnJLron^=pzS;*49O1NwK2Q|L(lQRu(@f3(!P&qGIE293_5^#&h?`AS4 zL9mPltJ%n$ByB*SvQCw*;)t^zdMSa?_Dpjgxvzw;;;+?|63Ed3*WEE$twMu2MTzFd z#jR3)*)IBu&c!wp(>zY?)Tj<8{z~8YLmPgl4_xJQRs(z%oPym~8Q@>1bqKs6K;6l7%Z?ji4~y3hKwO=*u%k?-Y~3yF{b=rc`OxQ)T{UX04tT1cjfONrbhRSX=+W!q zpe4Wr_UwG3)J#7E?G%^fgY)L?qbK+Le2u86{-O@!;xfALv>0HOHAXZ7xe17L(T-{M zs!h>&9=tlR{~B5!`7IKnPxJSa@w;+aywx2& zyniF0|2MMm0y{Twm)lX^-jMJFJK3keZykV^{WAN1gV6r1L*LGdf8zH3t3urI%F3!o z_iJ;p#?O{%*^j>uwtp_1d~f+N+scmaYz7A~;AG`^&$ciDTkd2vW>U_(AtGQ5x%7_A z#2P&59dtB+VefdC)IfD_V*Z1E8*m1AkFVE@YrVVoFyjAU6ZOP#;R!Oce;9Tv>j5rc z^VW~Qd*7i5_U5`2bQrM3{%dOe;&^c2{03-yyN1@RJ^KT&xx76Z8uTY(LrQP2i_fK5lzbiucr#k)13uqFQ`jZN0@R{uGC*PjX5rUwSp?j)d zG4NRP?;04`R@gMdaWYF7g1OPP&otN5K4uhO;3p&P3uY$P&Z;yRy{BigEife>%Pb;X zyr=EWombU5anC)P-(NJx&>s}QlbthdfJ;qUT1RQuxQnscB6>mzvI@~#`EHyGtCTuf zp0X<>B~03-U6$nh$6ial6bAkvQwNq%3H2JSdoZQrd1b&Hp7Nx1tG#Ecrk$3PH4@kH zVtxqO&YYu+AhPDz#Y_!tY* z$EvpbqFAU-5p|+#n@s(xle{JEkPDA&r3{YeBBpL(`26Rp3?^0a8SevvC^+QBtn~CD zCiB2Fs!3t2}1H7&brCd}t9u{lz>|UH3JR#^Vd|F%Ouo&}L3W`xK`LN#Uz-^jMTdd$I1eOZrmt9&)$9={q@TrZoYpMrV5hl@` zD`I1FBJr&4>Lo^Vhy$R(0iEvMLqrT}_T$^Cd8)CLN&x6Rlg??p>a-XjAFLoey>`cd!AHl5S;?@tO;#H{G zL@Eb7O3|!4cXu474q#r_&nxH~UoWAZU{;LEY0VBv{ZrMW_edXDO4U&(uopVOzRlQ= zS=2J{4z@tP+J%fXI3Lakd9Y6QR5cw;hFh_y!3t&;K1jX;s1fsggXYJfi7piy?0U8* zKh;jZtDH|E=j1PJK8RlGuH-6=GwSE|9q4CQ+G=lg1ZNK!8&tZ9-|@_F;DdSdX<7{7smSVcV?10zp|t9 z9N>msXFdW|1#vuw-B0e#W~MMj9&QXnGNi)|EY+@Cs;Pk-;U@siCt9NpEz+r+L_1mD zy?fWsHFV1P5s_RaZ1V8axw8S+(5B9O5ddhf>WW25Cl2 z>5up&8MQG3q*$6kRFN>KJ@8^@b_TP6?T+VQTHwij7rlC(`S^$*ko1=*jQ|HuE-Kz{ z6%oD_benTz^Eyn7-W<$^r`H-W%4OeXy2V~;G+UGKV0w(^-~qR^{PuB{?fMag1LRC6 zo1_28k9-Yjqh;}9^OdS087EjDF_1Vbd~J{0tG8K6EUV?*m5A8$E5E@0K-rT{)lB~A zB3XNkr{FqeS`T=dVB!#GYm!#ER(;sFA682J7?fq}$e@`OJnge|{-xSH*kM?&;60`V zy;_;!2x)O$8$NabajF}ovi!+0E`wqod-rTOEeIBzmrIz*5l;6AuTPp83x79&rKO$c zGqhI+BRheC`RB(4r6LXwa1-mxFH5eIt{bd8?(XO4^ML-o2b}~lZe(k8_lPQV@x=~_ z8FY*Iw>>w+g#3ID2V5pQU)=^S#Z%>iLHq(@<#4Rzj9){|c&C z|Melpmf$%~EQ4}1@!I51N#+zNgX&K$bEt1ma}s}p%kq5H+G=Yxb<)B^);<6N2-w5i zy?bxT&?sN6Q26}ff!o1K#%o|w)doj1kc+r~LBq!m{l1*JcD3EZdMNuLuvj!-k`fXW!j5nCnA$%j*E!n`ltnA$PM3SE!YNmmSru-r-Hy-K zyxu()KFe`2hYA90M$F?&!=ACJpH}>UkrKjYLu^cV;esA0p#QX-!&X5uoCd&Pzdj`C z5aftic$EC1YRaE<Xo{E{nIww1u#aeZHV8FL9SgSo9rM>*A|4{TEKpQQX^mj}Xyp&aU7M zp5sI;b=%i2n-IB9u!bIEIMpq9H(Nq;E!25em)FC1?B|?-y2p# zjT?hV1{F7J8lW;mR3)*r)$j{ zBmJ3XohrQROer%r#TCP@G3zOwg`CS$0_WWLP8f|{mA>qj&Ipv#Cw6F?M|wXOLHh;n zbrTvQl26(T6V<=1(|ZNTrBc&UxBRra(A#!rF4hy4WhQ9YB6{LYe<)hpAmSU0Tt&WM z0az$T*e;i)R&pqdhqaKSiTFtD3axrh_VdK+h^k^TL3o?27O5MpAX?6KBDj(TDsZxN zq!^t)V%wi7_V%NB_KujT3KGr- z`qY_qlH({{r(7ZD+IYAJiRF}|Qb}=jKL;h(QhA-?iLe?No|(EStwJVE-*~4CRLrR{ zCyKFljmSyT&29@X50@f5ul!UQj;r*&GZh#D!m}#1>0aaKym1?il1D=M_i|9``6F?9 ztrN=FA<=npk6l~_tG#{aJK(cN6@+b}uYi}C5qBaoSwLERzC2t0;tZ|F3)xwV5L3-l zbHbYMI`mGG20U#%3)L8@bekFxN+^a5Nw2H{bDEnsMyo#y0RYo;p_R>!4uRd%H4tnG zDH$FV9{DvFK2xfkTw5G^=@{2)MZvoqDeQ$U0~C!z-hKpJ|Byg$vilMZmLE`HNm*EL zS~w{p1C2|T^;(@7R;a^5S!zZhN??``}WuG-)DF(4D=(w6dvNO`l$`#bLgv z9GvThee9J?#4}Q`SY_W!AExf%ZjQS$Hq{#et@SklJ-3;Ka~NzCv3})we&%CaoYtPm z&2W1DOVE;Acjjsp<}w*OvKeJ>;Jk9CB;JG&(UTr~F(Zz$FvcE6uL;RjRsIT{mnlCj1NwKp&&q8I?@~H3 zmf9buSyp)+py-!S1~rc7=!22V@8OI^nuwdT+~V)_7~!4aEYka2R`b)$?jZqzAqP^g)S~_=j4|~2 z9!+6HSNUwiHKC)&!xub;Z|h#0AFcrt()uX%~mveOJYL{Po5Q#uEJ6<5Lz~lmc+erV3Su>~IizHKuypN=-}g23nlh zo7KDd?3fJg6@y{Lp7n5N<}%b-DBxR?Y1I_H_)yW|!O7-;B^0?W3eiDFtctvwqZ#T&q84zYG0L2Ko z8l?chR@iNk_XO<=R^lPCO)zcZ&^FCkmkOH58AUa14>00 zG&GUE9(~hOlGiIx1Qn5if!TN!v@^;ebMm8wI!=Mg&js-&QY`#O*cWHx(xbeu{-rG$hnnvq|8q4foX@s>5UK_w3F{+U#-ymc!@QJJ^ zGT)o09uHP(fFD;prF@Xzr#W0 zcr!sSLrO3I75Wb&Mxf{@KAz%>=t+9KxzGl20>y}hC|EAH^ib3*udBNp$eog*RaRVC zh0GhIvmDVnB8hhH`4!#Z}==rKc)9=~d(1eIGY5OS&P9v?r<6wMhRS#`F( zFd7F$-hjQz!#}9_8Bh`B6$R|eLTVsdE zzW&mB{16#A#Cz}fBft9y=j(dClSk&Ve9O=3AD;zoL)z}ol6+;dnumC$;d=$ zA1wEJ94zkx1~i?DOo&sv$-wL2>-vrDX@_@&6MIv^+Af0e!Dn z3^R3gtG`3e$n)vOalc5d#!Lr+3`xMH;Pr}pp~6S&JtScSm;p!O#hzho?NUN~v#F`a zxp(>Wx=X63f%yFwN2hwKxqp`vy?f2hb7L9#LJb&&$z;dW80D5NyHtv?PWJUvhp$1x zq~bFV=5AK8)H8HudS$XX;$+sN?4PkF$<%~AEt{5WRD(XIGvVKn6%dIr7V2lF6hnF* z~pKvg%v86j}B0s7CAZr80k^#$yy3wiKCgdI+u*Ob-Lt@k8hr9Raw(kQSz1)Uuo~= zs4xWQM#1Qfl#PWxU79P{eTdBJJ>9(sQB$n8i_mh~*qKA{bhFdbp1R(M)gKOCh9+|M z>c89?kD$7#rG6C9+&&iT=1pg(*TB-!YWC&pnbnN<7nCD>&;SA?(R%qm<(-SM-nkdqB#3Z%3_N& zZ6ve8FVdYhO&cFt9!4U1DOl4r|+IKGgY7Sa$@@)_|0C8@O(+C_Rene5vkROrAESXomsbGaeIRHMA5WAmXkBJ~wC%yZGj-wFrT za{4^an>tIHsCBE@ZTd5XRJ}N$+z$bSU!`mSJ}Q>rh1S~!cVf&7vzkwe3+X14E?ZRc zK(u=hKvd^;wTKF#1+pTEJdh_f$s1Ki$zhtq1w)s8g_|lLvi)9~Fn(A{p{$j02T(`n zV1Zm(FU5BnjTy`5_~Ybzq5}iDOYLFAfJ;Vt!L;Z=6|jB^qr5mBr^jikF4YGR(J}#5 z0{mMi%>np8+hh3TvrA5wwGw%Vez{NBJG}-+aMM{aiP^+SN17jIQ&Uby53iVgqp1Iq zVL3F7&1Ll4LQ1GsrHv8L#Xn=3fogAUz21y@A?%tu_qnlf3VuFtF%!S)d(CE|3Ly`+ z9AettL9j5YwDS44?_AN(%o;dfx%o7`=_Xso=qY13@A~p0+Zn3S&4>LdU#+NKwJ7Rz z?_H=cs`^;FXf{-qfS#c|SWoOk?mVw6Pk#<(S7(GgUeGT{hfQFHTh~!dRWVeXOiAgH zlz4CU3JWn7%-BV#yM7uBj#4r4xv|H^k3S%)_7wySR(A#7Jg|# z;q@gjG{{kRc>-l~#LU3ZC1#z=iJtNe3MVzZdallz9DE-!oO3Qv%;RylxBIy|_FTp%60J$vPXSLwx=|p)3Mhy5Ih2mx9}Z z7*U9{col+De} zeodb4=p{AU0}GRWQW}r>^#)zGgY$Ym`{Km^b)99b6bmM#rS-#TJQd|PqE`bcP-u^VhHnN;KOFb)`k)2)i zf~vJmAg7#<@;uoo1TVlKAl`n>!Q28(A0$!&hbUCWfBI^z=%|n};@CSU@UByv%7!-Z zn^tZoYoT<7W~jP3xG$7Vlf#BiR#fZ7$*&Y49Fgxmm&}$ZA*lOv1f4~2PZfv$Sqy7>&rykbTB9c$Df^V{kl?G#UfO1 z_ucunV)+^N7k5{8Ny*tLB&!z=Vae5{ANXV|h2`RST{GHy=P@UEZ3X$CJvcMGBAdcY)i?r&b55Gu*KIRub0R_UKeTW9|--NDyUdx zYh3f-jrC8y=BS#8Q8|&=_RD$p!xI43Is`Roh88>Uq5k_J-e6C08>0`*0D}WlCS!?g%Cqvx-LWf0e@GEg{~ru{|l-@i4F!Ctn??`cntod z%=}rV4YY65IX9QKUE1Y~123Lho#H0_2Y=0&42iI@eH{IAp|>ig=JmX*V&Da);sJXi z@qw;=is5*5*N}AoEEww2tAXWeJ)JXukVuyOT$xFo496a@P(Is(zcYT3#HpmUZnd9o z^CCYhP!5uvSvmpNGZs{0C%kK`mRq5uN@AuBL>-rs6!6v15kqHP=tkP&QuBPmuNH@Pim?Uf_xGC@xq0NUstH{aNsRcXd? z#okAz&$jmlP^4=9XqA1Ke_-~W-VH<_h2k3x5zTdcGo<+grGeS*st`v=;uHCmUI1I% zfnk+8!3{jlp8X`+8C`WV`=FVLl{uJz{#1!?%#964k0MmYAVHOWw=kq(Uu)f$h>2Fj z(fX-VR@`>dOk0OovL@!SL2fY!^`e$JI)QK1+8ccJXNCh%BRE_Dk~M&uxP1fA-oV0F z=T_M|{g@m*)f4VUGqmlHTslhP9xoFbADkbRrn%y?KwZE|)jeI8tC$6w(T}b89w4bQ z8lMh!9uYA$yQ!H)RIOfrE$thM-nGUXWCc!%v5W&;sm&sZ^4d^u*xGGRD@M)zzvbQk z&b8{_OJ}zq{Y#AG3>VH-yj=`W{48>o2(n}M|mRK%;piW|(ykG?MMUY}lylwJ*| z6e&w|GX-A7LmPOSq|L?2@-l&))9C{nh_jVdWB~kd{wYnbo=AbMCD74oMW>ksTDKpO zZL0usVZmW=DdtyhO`?ayUDK-@FlD#HXKr@+i$)p1Gc25Zvh^3_AI8UJA?(ZA$!IVB zy|}dje(o7;fIbSe;zp-gTUHEt zBdd}>aW)yvVZS2__U14Z9tJ|4Y~X5u1Dk5c(C|Fb+^o&tc zF|Atv?D(&w_%RYrOo{l#jXvsN_okvgH&1+a&{dXn`70yL`czw zU9Fc)>B{>r(|<6~hC1_pamdCOwwN*>k@?xj^>q~=nB&+F+DvZMkYD+}>;N;WVl%DR zV!#~0L!UH4iKW|?_wUijXmh!sb27hZSghZ4o{AvPH+nknac-|tCOw9z@0`z!&mPqS zX=W#~im>}PPX*L=+V1=4SGc=9KFA16 z-@6*~8d8dUZkA<@mN4VPSnX7IZQLqeX0!!YpgYTXbOa%S8+{MmSo{K9Vxi5#)W0rKO(h zF#(3-Nlyu1ydbNO^Rc>*(@8)^(gP*U5rY;t zg1ik(@(yCWs%5$3bv&4x$3;5uzLnuPr*HZyBf?$)bA1uUNIqM|CCmbnL%ja;Qfc&ps#&Zzk~q8zMZTx|&>Oan3#q7hR>pL4ThVMYnk-fMu%Zl25t z)15e+rYRm~7;7FFa((RE@OrZNi6r##W#fBDWiaak+Tg1|Ib*_2ZY?-+s3k zuJj5DnKE%Ts14LmBx3z$VmzfSbi?hcYeKS@j(6+D&G)re>gFV7{n1nmBXab3yE9(9 z-xia#)m~GBFqbQ$UXST0nSC{(=x*nk5821fmVNGdzS#L|9)9)Jo3={1wr^TCSIMLy zHFm}n==jNuuGNysI+840Y~9$}!@$6qlzRV8#kpmqkc>E%L3_mM+TmpVn1ap4mTj+T z;ZKd=FQ24S zGZ;5KQZvOqwJFjn|0Jrz+1i~WurOl5T^7d?N2{LcdcJ45%9PxcI?IUqagH{oTy6jln@Y#&zXW}!U*y2xA zL+8kemP4Gv(eLM%2CfDQcmAqu+X8(2&5B7m&_!J0SjHTeDod zVSkb=;I#(iOaYBtraXnO6!7it-hQ$WOP2%T@p7^N8NOA30s*#DXgiH*Ik>Cp9#&{v zLfrGsB?7y{wpJ0BfYoG&G7Px zxiMBcA?A37Uk74^eQzuE$2wLe(-s+eU$l@VRs zcdP&ircukfiOK$;!p_KIJ5Q@&M8o$0(!&{BLb%cHtig0!O=8!U%k00F_sFF|RR4>o zxctczVhoJykyeNHNx&_udy18s@0Gw60m1dY{HL}I`P^O^!qy)DiEJw7*DUBXdBy5p zom}t!C02Qp%DCnf&*kz0Kr1bEv8_`@_o@Q5wf3O1kq|s%{NE^O6G{N;@HXshB?KQR zl-*ouD%OuXY+l8?l)OC>2P6;NwR<2@w3o!V&&{iUQz7*G&i=R1hu{B-VdL}HdvKm) zdUUBo>w{9eN~H|1r(jFTP)Y?Pp-{ zk>|E|ul!Yphw)g^_#1Jbai1>YDG*KFZpb@c{XFpwiZEOZ3IIy>dmo0)KZ26qkd43g z{Wr#-|0cwmf8J5prJmFA*XMpY_~HOd+S~2_{1iZG(qA8``uzXq|MZ^|-j3)0-S_&R zG5pUM-qzUv-u^R}e-FdI@{;~ps`$^DvIjZYe0zK3+wX|um$BlvB>P{!x4{3Bm%o7B zO!sfc6Zb#6|34cOQQ%v>xw&}|5Ci;xbsOd0!?^cDQVgfi-~`x%Atf&NzDV?=@BaY7 Can=I> literal 0 HcmV?d00001 diff --git a/docs/images/live_show/live_plan_modal_crew.png b/docs/images/live_show/live_plan_modal_crew.png new file mode 100644 index 0000000000000000000000000000000000000000..9142d3a987bcf320f48bb44c66990c5a7950fc0f GIT binary patch literal 62758 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 None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "crew_assignment", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("crew_id", sa.Integer(), nullable=False), + sa.Column("scene_id", sa.Integer(), nullable=False), + sa.Column("assignment_type", sa.String(), nullable=False), + sa.Column("prop_id", sa.Integer(), nullable=True), + sa.Column("scenery_id", sa.Integer(), nullable=True), + sa.CheckConstraint( + "(prop_id IS NOT NULL AND scenery_id IS NULL) OR (prop_id IS NULL AND scenery_id IS NOT NULL)", + name=op.f("ck_crew_assignment_exactly_one_item_type"), + ), + sa.ForeignKeyConstraint( + ["crew_id"], + ["crew.id"], + name=op.f("fk_crew_assignment_crew_id_crew"), + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + ["prop_id"], + ["props.id"], + name=op.f("fk_crew_assignment_prop_id_props"), + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + ["scene_id"], + ["scene.id"], + name=op.f("fk_crew_assignment_scene_id_scene"), + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + ["scenery_id"], + ["scenery.id"], + name=op.f("fk_crew_assignment_scenery_id_scenery"), + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_crew_assignment")), + sa.UniqueConstraint( + "crew_id", + "scene_id", + "assignment_type", + "prop_id", + name="uq_crew_prop_assignment", + ), + sa.UniqueConstraint( + "crew_id", + "scene_id", + "assignment_type", + "scenery_id", + name="uq_crew_scenery_assignment", + ), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("crew_assignment") + # ### end Alembic commands ### diff --git a/server/controllers/api/constants.py b/server/controllers/api/constants.py index 0e38529c..b487758e 100644 --- a/server/controllers/api/constants.py +++ b/server/controllers/api/constants.py @@ -47,6 +47,9 @@ # Allocations ERROR_ALLOCATION_NOT_FOUND = "404 allocation not found" +# Crew assignments +ERROR_CREW_ASSIGNMENT_NOT_FOUND = "404 crew assignment not found" + # ============================================================================= # HTTP 400 Validation Errors - Missing Required Fields @@ -92,6 +95,15 @@ ERROR_PROPS_ID_MISSING = "props_id missing" ERROR_SCENERY_ID_MISSING = "scenery_id missing" +# Crew assignments +ERROR_CREW_ID_MISSING = "crew_id missing" +ERROR_ASSIGNMENT_TYPE_MISSING = "assignment_type missing" +ERROR_ASSIGNMENT_TYPE_INVALID = "assignment_type must be 'set' or 'strike'" +ERROR_ITEM_ID_MISSING = "Either prop_id or scenery_id must be provided" +ERROR_ITEM_ID_BOTH = "Only one of prop_id or scenery_id can be provided" +ERROR_INVALID_BOUNDARY = "Scene is not a valid boundary for this assignment type" +ERROR_CREW_ASSIGNMENT_EXISTS = "Crew assignment already exists" + # ============================================================================= # HTTP 400 Conflict/Business Rule Errors diff --git a/server/controllers/api/show/stage/crew_assignments.py b/server/controllers/api/show/stage/crew_assignments.py new file mode 100644 index 00000000..c6d83af8 --- /dev/null +++ b/server/controllers/api/show/stage/crew_assignments.py @@ -0,0 +1,474 @@ +""" +API controller for crew assignments to props/scenery items. + +Crew assignments track which crew members are responsible for setting (bringing on stage) +or striking (removing from stage) props and scenery items at specific scene boundaries. +""" + +from sqlalchemy import select +from tornado import escape + +from controllers.api.constants import ( + ERROR_ASSIGNMENT_TYPE_INVALID, + ERROR_ASSIGNMENT_TYPE_MISSING, + ERROR_CREW_ASSIGNMENT_EXISTS, + ERROR_CREW_ASSIGNMENT_NOT_FOUND, + ERROR_CREW_ID_MISSING, + ERROR_CREW_NOT_FOUND, + ERROR_ID_MISSING, + ERROR_INVALID_BOUNDARY, + ERROR_INVALID_ID, + ERROR_ITEM_ID_BOTH, + ERROR_ITEM_ID_MISSING, + ERROR_PROP_NOT_FOUND, + ERROR_SCENE_ID_MISSING, + ERROR_SCENE_NOT_FOUND, + ERROR_SCENERY_NOT_FOUND, + ERROR_SHOW_NOT_FOUND, +) +from models.show import Scene, Show +from models.stage import Crew, CrewAssignment, Props, Scenery +from rbac.role import Role +from schemas.schemas import CrewAssignmentSchema +from utils.show.block_computation import is_valid_boundary +from utils.web.base_controller import BaseAPIController +from utils.web.route import ApiRoute, ApiVersion +from utils.web.web_decorators import no_live_session, requires_show + + +@ApiRoute("show/stage/crew/assignments", ApiVersion.V1) +class CrewAssignmentController(BaseAPIController): + """Controller for crew assignment CRUD operations.""" + + @requires_show + def get(self): + """Get all crew assignments for the current show.""" + current_show = self.get_current_show() + show_id = current_show["id"] + schema = CrewAssignmentSchema() + + with self.make_session() as session: + show = session.get(Show, show_id) + if not show: + self.set_status(404) + self.finish({"message": ERROR_SHOW_NOT_FOUND}) + return + + # Get all assignments for crew members in this show + assignments = session.scalars( + select(CrewAssignment).join(Crew).where(Crew.show_id == show_id) + ).all() + + result = [schema.dump(a) for a in assignments] + self.set_status(200) + self.finish({"assignments": result}) + + @requires_show + @no_live_session + async def post(self): + """ + Create a new crew assignment. + + Required body fields: + - crew_id: ID of the crew member + - scene_id: ID of the scene (must be a valid block boundary) + - assignment_type: 'set' or 'strike' + - prop_id OR scenery_id: ID of the item (exactly one required) + """ + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if not show: + self.set_status(404) + await self.finish({"message": ERROR_SHOW_NOT_FOUND}) + return + + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + # Validate crew_id + crew_id = data.get("crew_id") + if crew_id is None: + self.set_status(400) + await self.finish({"message": ERROR_CREW_ID_MISSING}) + return + + try: + crew_id = int(crew_id) + except ValueError: + self.set_status(400) + await self.finish({"message": ERROR_INVALID_ID}) + return + + crew = session.get(Crew, crew_id) + if not crew or crew.show_id != show_id: + self.set_status(404) + await self.finish({"message": ERROR_CREW_NOT_FOUND}) + return + + # Validate scene_id + scene_id = data.get("scene_id") + if scene_id is None: + self.set_status(400) + await self.finish({"message": ERROR_SCENE_ID_MISSING}) + return + + try: + scene_id = int(scene_id) + except ValueError: + self.set_status(400) + await self.finish({"message": ERROR_INVALID_ID}) + return + + scene = session.get(Scene, scene_id) + if not scene or scene.show_id != show_id: + self.set_status(404) + await self.finish({"message": ERROR_SCENE_NOT_FOUND}) + return + + # Validate assignment_type + assignment_type = data.get("assignment_type") + if not assignment_type: + self.set_status(400) + await self.finish({"message": ERROR_ASSIGNMENT_TYPE_MISSING}) + return + + if assignment_type not in ("set", "strike"): + self.set_status(400) + await self.finish({"message": ERROR_ASSIGNMENT_TYPE_INVALID}) + return + + # Validate prop_id/scenery_id (exactly one must be provided) + prop_id = data.get("prop_id") + scenery_id = data.get("scenery_id") + + if prop_id is None and scenery_id is None: + self.set_status(400) + await self.finish({"message": ERROR_ITEM_ID_MISSING}) + return + + if prop_id is not None and scenery_id is not None: + self.set_status(400) + await self.finish({"message": ERROR_ITEM_ID_BOTH}) + return + + # Validate the item exists and belongs to the show + if prop_id is not None: + try: + prop_id = int(prop_id) + except ValueError: + self.set_status(400) + await self.finish({"message": ERROR_INVALID_ID}) + return + + prop = session.get(Props, prop_id) + if not prop or prop.show_id != show_id: + self.set_status(404) + await self.finish({"message": ERROR_PROP_NOT_FOUND}) + return + scenery_id = None + else: + try: + scenery_id = int(scenery_id) + except ValueError: + self.set_status(400) + await self.finish({"message": ERROR_INVALID_ID}) + return + + scenery = session.get(Scenery, scenery_id) + if not scenery or scenery.show_id != show_id: + self.set_status(404) + await self.finish({"message": ERROR_SCENERY_NOT_FOUND}) + return + prop_id = None + + # Validate that the scene is a valid boundary for this assignment + if not is_valid_boundary( + session, scene_id, assignment_type, prop_id, scenery_id, show + ): + self.set_status(400) + await self.finish({"message": ERROR_INVALID_BOUNDARY}) + return + + # Check for duplicate assignment + existing_query = select(CrewAssignment).where( + CrewAssignment.crew_id == crew_id, + CrewAssignment.scene_id == scene_id, + CrewAssignment.assignment_type == assignment_type, + ) + if prop_id is not None: + existing_query = existing_query.where(CrewAssignment.prop_id == prop_id) + else: + existing_query = existing_query.where( + CrewAssignment.scenery_id == scenery_id + ) + + existing = session.scalars(existing_query).first() + if existing: + self.set_status(400) + await self.finish({"message": ERROR_CREW_ASSIGNMENT_EXISTS}) + return + + # Create the assignment + assignment = CrewAssignment( + crew_id=crew_id, + scene_id=scene_id, + assignment_type=assignment_type, + prop_id=prop_id, + scenery_id=scenery_id, + ) + session.add(assignment) + session.commit() + + self.set_status(200) + await self.finish( + {"id": assignment.id, "message": "Successfully created crew assignment"} + ) + + await self.application.ws_send_to_all("NOOP", "GET_CREW_ASSIGNMENTS", {}) + + @requires_show + @no_live_session + async def patch(self): + """ + Update an existing crew assignment. + + Required body fields: + - id: ID of the assignment to update + + Optional body fields (provide any to update): + - crew_id: New crew member ID + - scene_id: New scene ID (must be valid boundary for item/type) + - assignment_type: New assignment type ('set' or 'strike') + - prop_id: New prop ID (clears scenery_id) + - scenery_id: New scenery ID (clears prop_id) + """ + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if not show: + self.set_status(404) + await self.finish({"message": ERROR_SHOW_NOT_FOUND}) + return + + self.requires_role(show, Role.WRITE) + data = escape.json_decode(self.request.body) + + # Get the assignment to update + assignment_id = data.get("id") + if assignment_id is None: + self.set_status(400) + await self.finish({"message": ERROR_ID_MISSING}) + return + + try: + assignment_id = int(assignment_id) + except ValueError: + self.set_status(400) + await self.finish({"message": ERROR_INVALID_ID}) + return + + assignment = session.get(CrewAssignment, assignment_id) + if not assignment: + self.set_status(404) + await self.finish({"message": ERROR_CREW_ASSIGNMENT_NOT_FOUND}) + return + + # Verify the assignment belongs to this show + crew = session.get(Crew, assignment.crew_id) + if not crew or crew.show_id != show_id: + self.set_status(404) + await self.finish({"message": ERROR_CREW_ASSIGNMENT_NOT_FOUND}) + return + + # Collect the new values (use existing if not provided) + new_crew_id = assignment.crew_id + new_scene_id = assignment.scene_id + new_assignment_type = assignment.assignment_type + new_prop_id = assignment.prop_id + new_scenery_id = assignment.scenery_id + + # Update crew_id if provided + if "crew_id" in data: + try: + new_crew_id = int(data["crew_id"]) + except (ValueError, TypeError): + self.set_status(400) + await self.finish({"message": ERROR_INVALID_ID}) + return + + new_crew = session.get(Crew, new_crew_id) + if not new_crew or new_crew.show_id != show_id: + self.set_status(404) + await self.finish({"message": ERROR_CREW_NOT_FOUND}) + return + + # Update scene_id if provided + if "scene_id" in data: + try: + new_scene_id = int(data["scene_id"]) + except (ValueError, TypeError): + self.set_status(400) + await self.finish({"message": ERROR_INVALID_ID}) + return + + new_scene = session.get(Scene, new_scene_id) + if not new_scene or new_scene.show_id != show_id: + self.set_status(404) + await self.finish({"message": ERROR_SCENE_NOT_FOUND}) + return + + # Update assignment_type if provided + if "assignment_type" in data: + new_assignment_type = data["assignment_type"] + if new_assignment_type not in ("set", "strike"): + self.set_status(400) + await self.finish({"message": ERROR_ASSIGNMENT_TYPE_INVALID}) + return + + # Update prop_id/scenery_id if provided (XOR logic) + if "prop_id" in data or "scenery_id" in data: + new_prop_id = data.get("prop_id") + new_scenery_id = data.get("scenery_id") + + if new_prop_id is None and new_scenery_id is None: + self.set_status(400) + await self.finish({"message": ERROR_ITEM_ID_MISSING}) + return + + if new_prop_id is not None and new_scenery_id is not None: + self.set_status(400) + await self.finish({"message": ERROR_ITEM_ID_BOTH}) + return + + if new_prop_id is not None: + try: + new_prop_id = int(new_prop_id) + except (ValueError, TypeError): + self.set_status(400) + await self.finish({"message": ERROR_INVALID_ID}) + return + + prop = session.get(Props, new_prop_id) + if not prop or prop.show_id != show_id: + self.set_status(404) + await self.finish({"message": ERROR_PROP_NOT_FOUND}) + return + new_scenery_id = None + else: + try: + new_scenery_id = int(new_scenery_id) + except (ValueError, TypeError): + self.set_status(400) + await self.finish({"message": ERROR_INVALID_ID}) + return + + scenery = session.get(Scenery, new_scenery_id) + if not scenery or scenery.show_id != show_id: + self.set_status(404) + await self.finish({"message": ERROR_SCENERY_NOT_FOUND}) + return + new_prop_id = None + + # Validate the new combination is a valid boundary + if not is_valid_boundary( + session, + new_scene_id, + new_assignment_type, + new_prop_id, + new_scenery_id, + show, + ): + self.set_status(400) + await self.finish({"message": ERROR_INVALID_BOUNDARY}) + return + + # Check for duplicate assignment (excluding self) + existing_query = select(CrewAssignment).where( + CrewAssignment.id != assignment_id, + CrewAssignment.crew_id == new_crew_id, + CrewAssignment.scene_id == new_scene_id, + CrewAssignment.assignment_type == new_assignment_type, + ) + if new_prop_id is not None: + existing_query = existing_query.where( + CrewAssignment.prop_id == new_prop_id + ) + else: + existing_query = existing_query.where( + CrewAssignment.scenery_id == new_scenery_id + ) + + existing = session.scalars(existing_query).first() + if existing: + self.set_status(400) + await self.finish({"message": ERROR_CREW_ASSIGNMENT_EXISTS}) + return + + # Apply updates + assignment.crew_id = new_crew_id + assignment.scene_id = new_scene_id + assignment.assignment_type = new_assignment_type + assignment.prop_id = new_prop_id + assignment.scenery_id = new_scenery_id + + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully updated crew assignment"}) + + await self.application.ws_send_to_all("NOOP", "GET_CREW_ASSIGNMENTS", {}) + + @requires_show + @no_live_session + async def delete(self): + """Delete a crew assignment by ID (query parameter).""" + current_show = self.get_current_show() + show_id = current_show["id"] + + with self.make_session() as session: + show = session.get(Show, show_id) + if not show: + self.set_status(404) + await self.finish({"message": ERROR_SHOW_NOT_FOUND}) + return + + self.requires_role(show, Role.WRITE) + + assignment_id_str = self.get_argument("id", None) + if not assignment_id_str: + self.set_status(400) + await self.finish({"message": ERROR_ID_MISSING}) + return + + try: + assignment_id = int(assignment_id_str) + except ValueError: + self.set_status(400) + await self.finish({"message": ERROR_INVALID_ID}) + return + + assignment = session.get(CrewAssignment, assignment_id) + if not assignment: + self.set_status(404) + await self.finish({"message": ERROR_CREW_ASSIGNMENT_NOT_FOUND}) + return + + # Verify the assignment belongs to a crew member in this show + crew = session.get(Crew, assignment.crew_id) + if not crew or crew.show_id != show_id: + self.set_status(404) + await self.finish({"message": ERROR_CREW_ASSIGNMENT_NOT_FOUND}) + return + + session.delete(assignment) + session.commit() + + self.set_status(200) + await self.finish({"message": "Successfully deleted crew assignment"}) + + await self.application.ws_send_to_all("NOOP", "GET_CREW_ASSIGNMENTS", {}) diff --git a/server/controllers/api/show/stage/helpers.py b/server/controllers/api/show/stage/helpers.py index b1c985da..8bb2b6be 100644 --- a/server/controllers/api/show/stage/helpers.py +++ b/server/controllers/api/show/stage/helpers.py @@ -18,7 +18,12 @@ ERROR_SHOW_NOT_FOUND, ) from models.show import Scene, Show +from models.stage import Props, Scenery from rbac.role import Role +from utils.show.block_computation import ( + delete_orphaned_assignments_for_prop, + delete_orphaned_assignments_for_scenery, +) async def handle_type_post(controller, type_model, ws_action, success_message): @@ -319,6 +324,25 @@ async def handle_allocation_post( **{allocation_item_fk: item_id, "scene_id": scene_id} ) session.add(new_allocation) + # Flush to persist the allocation within the current transaction, + # making it available for block computation without committing yet + session.flush() + + # Refresh the item to get updated relationships for orphan detection + session.refresh(item) + + # Delete any crew assignments that are now orphaned due to block boundary changes + deleted_assignment_ids = [] + if isinstance(item, Props): + deleted_assignment_ids = delete_orphaned_assignments_for_prop( + session, item, show + ) + elif isinstance(item, Scenery): + deleted_assignment_ids = delete_orphaned_assignments_for_scenery( + session, item, show + ) + + # Commit the entire operation atomically (allocation + orphan deletions) session.commit() controller.set_status(200) @@ -328,6 +352,12 @@ async def handle_allocation_post( await controller.application.ws_send_to_all("NOOP", ws_action, {}) + # Also notify about crew assignment changes if any were deleted + if deleted_assignment_ids: + await controller.application.ws_send_to_all( + "NOOP", "GET_CREW_ASSIGNMENTS", {} + ) + async def handle_allocation_delete( controller, @@ -385,9 +415,34 @@ async def handle_allocation_delete( return session.delete(allocation) + # Flush to persist the deletion within the current transaction, + # making it available for block computation without committing yet + session.flush() + + # Refresh the item to get updated relationships for orphan detection + session.refresh(item) + + # Delete any crew assignments that are now orphaned due to block boundary changes + deleted_assignment_ids = [] + if isinstance(item, Props): + deleted_assignment_ids = delete_orphaned_assignments_for_prop( + session, item, show + ) + elif isinstance(item, Scenery): + deleted_assignment_ids = delete_orphaned_assignments_for_scenery( + session, item, show + ) + + # Commit the entire operation atomically (allocation deletion + orphan deletions) session.commit() controller.set_status(200) await controller.finish({"message": "Successfully deleted allocation"}) await controller.application.ws_send_to_all("NOOP", ws_action, {}) + + # Also notify about crew assignment changes if any were deleted + if deleted_assignment_ids: + await controller.application.ws_send_to_all( + "NOOP", "GET_CREW_ASSIGNMENTS", {} + ) diff --git a/server/controllers/api/show/stage/scenery.py b/server/controllers/api/show/stage/scenery.py index 72004b84..aa673e5e 100644 --- a/server/controllers/api/show/stage/scenery.py +++ b/server/controllers/api/show/stage/scenery.py @@ -2,7 +2,6 @@ from tornado import escape from controllers.api.constants import ( - ERROR_CAST_MEMBER_NOT_FOUND, ERROR_ID_MISSING, ERROR_INVALID_ID, ERROR_NAME_MISSING, @@ -126,7 +125,7 @@ async def post(self): scenery_type_id = int(scenery_type_id) except ValueError: self.set_status(400) - await self.finish({"message": "Scenery prop type ID"}) + await self.finish({"message": "Invalid scenery type ID"}) return scenery_type: SceneryType = session.get(SceneryType, scenery_type_id) if not scenery_type: @@ -195,7 +194,7 @@ async def patch(self): scenery_type_id = int(scenery_type_id) except ValueError: self.set_status(400) - await self.finish({"message": "Scenery prop type ID"}) + await self.finish({"message": "Invalid scenery type ID"}) return scenery_type: SceneryType = session.get( SceneryType, scenery_type_id @@ -223,7 +222,7 @@ async def patch(self): ) else: self.set_status(404) - await self.finish({"message": ERROR_CAST_MEMBER_NOT_FOUND}) + await self.finish({"message": ERROR_SCENERY_NOT_FOUND}) return else: self.set_status(404) diff --git a/server/models/show.py b/server/models/show.py index 7ba93f54..3ee2e5ac 100644 --- a/server/models/show.py +++ b/server/models/show.py @@ -17,6 +17,7 @@ from models.session import ShowSession from models.stage import ( Crew, + CrewAssignment, Props, PropsAllocation, PropType, @@ -228,3 +229,7 @@ class Scene(db.Model): back_populates="scene", cascade="all, delete-orphan", ) + crew_assignments: Mapped[List["CrewAssignment"]] = relationship( + back_populates="scene", + cascade="all, delete-orphan", + ) diff --git a/server/models/stage.py b/server/models/stage.py index 795aefd3..a5cb6ad2 100644 --- a/server/models/stage.py +++ b/server/models/stage.py @@ -2,7 +2,7 @@ from typing import List -from sqlalchemy import ForeignKey, UniqueConstraint +from sqlalchemy import CheckConstraint, ForeignKey, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from models.models import db @@ -18,6 +18,67 @@ class Crew(db.Model): last_name: Mapped[str | None] = mapped_column() show: Mapped[Show] = relationship(back_populates="crew_list") + crew_assignments: Mapped[List["CrewAssignment"]] = relationship( + back_populates="crew", + cascade="all, delete-orphan", + ) + + +class CrewAssignment(db.Model): + """ + Assigns a crew member to SET or STRIKE an item (prop or scenery) in a specific scene. + + The scene must be a block boundary for the item: + - SET assignments go on the first scene of a block + - STRIKE assignments go on the last scene of a block + + Exactly one of prop_id or scenery_id must be set (enforced by CHECK constraint). + """ + + __tablename__ = "crew_assignment" + __table_args__ = ( + # Exactly one of prop_id or scenery_id must be set + CheckConstraint( + "(prop_id IS NOT NULL AND scenery_id IS NULL) OR " + "(prop_id IS NULL AND scenery_id IS NOT NULL)", + name="exactly_one_item_type", + ), + # Prevent duplicate assignments for props + UniqueConstraint( + "crew_id", + "scene_id", + "assignment_type", + "prop_id", + name="uq_crew_prop_assignment", + ), + # Prevent duplicate assignments for scenery + UniqueConstraint( + "crew_id", + "scene_id", + "assignment_type", + "scenery_id", + name="uq_crew_scenery_assignment", + ), + ) + + id: Mapped[int] = mapped_column(primary_key=True) + crew_id: Mapped[int] = mapped_column(ForeignKey("crew.id", ondelete="CASCADE")) + scene_id: Mapped[int] = mapped_column(ForeignKey("scene.id", ondelete="CASCADE")) + assignment_type: Mapped[str] = mapped_column() # 'set' or 'strike' + + # Two nullable FKs - exactly one must be non-null + prop_id: Mapped[int | None] = mapped_column( + ForeignKey("props.id", ondelete="CASCADE") + ) + scenery_id: Mapped[int | None] = mapped_column( + ForeignKey("scenery.id", ondelete="CASCADE") + ) + + # Relationships + crew: Mapped["Crew"] = relationship(back_populates="crew_assignments") + scene: Mapped[Scene] = relationship(back_populates="crew_assignments") + prop: Mapped["Props | None"] = relationship(back_populates="crew_assignments") + scenery: Mapped["Scenery | None"] = relationship(back_populates="crew_assignments") class SceneryAllocation(db.Model): @@ -90,6 +151,10 @@ class Scenery(db.Model): back_populates="scenery", cascade="all, delete-orphan", ) + crew_assignments: Mapped[List["CrewAssignment"]] = relationship( + back_populates="scenery", + cascade="all, delete-orphan", + ) class PropType(db.Model): @@ -122,3 +187,7 @@ class Props(db.Model): back_populates="prop", cascade="all, delete-orphan", ) + crew_assignments: Mapped[List["CrewAssignment"]] = relationship( + back_populates="prop", + cascade="all, delete-orphan", + ) diff --git a/server/schemas/schemas.py b/server/schemas/schemas.py index d6026206..cee93003 100644 --- a/server/schemas/schemas.py +++ b/server/schemas/schemas.py @@ -16,6 +16,7 @@ from models.show import Act, Cast, Character, CharacterGroup, Scene, Show from models.stage import ( Crew, + CrewAssignment, Props, PropsAllocation, PropType, @@ -137,6 +138,14 @@ class Meta: include_fk = True +@schema +class CrewAssignmentSchema(SQLAlchemyAutoSchema): + class Meta: + model = CrewAssignment + load_instance = True + include_fk = True + + @schema class CharacterSchema(SQLAlchemyAutoSchema): class Meta: diff --git a/server/test/controllers/api/show/stage/test_crew_assignments.py b/server/test/controllers/api/show/stage/test_crew_assignments.py new file mode 100644 index 00000000..fc90bb81 --- /dev/null +++ b/server/test/controllers/api/show/stage/test_crew_assignments.py @@ -0,0 +1,837 @@ +"""Unit tests for crew assignments API controller.""" + +import tornado.escape +from tornado.httpclient import HTTPRequest +from tornado.testing import gen_test + +from models.stage import ( + Crew, + CrewAssignment, + Props, + PropsAllocation, + Scenery, + SceneryAllocation, +) +from test.conftest import DigiScriptTestCase +from test.helpers.stage_fixtures import ( + create_act_with_scenes, + create_admin_user, + create_crew, + create_prop, + create_scenery, + create_show, +) + + +class TestCrewAssignmentController(DigiScriptTestCase): + """Test suite for /api/v1/show/stage/crew/assignments endpoint.""" + + def setUp(self): + super().setUp() + with self._app.get_db().sessionmaker() as session: + self.show_id = create_show(session) + _, scene_ids = create_act_with_scenes( + session, + self.show_id, + "Act 1", + 3, + link_to_show=True, + ) + self.scene1_id, self.scene2_id, self.scene3_id = scene_ids + + self.crew_id = create_crew(session, self.show_id) + self.prop_type_id, self.prop_id = create_prop(session, self.show_id) + self.scenery_type_id, self.scenery_id = create_scenery( + session, self.show_id + ) + + # Allocate prop to scenes 1 and 2 (forms one block) + # SET boundary: scene 1, STRIKE boundary: scene 2 + allocation1 = PropsAllocation( + props_id=self.prop_id, scene_id=self.scene1_id + ) + allocation2 = PropsAllocation( + props_id=self.prop_id, scene_id=self.scene2_id + ) + session.add_all([allocation1, allocation2]) + + # Allocate scenery to scene 3 only (single-scene block) + allocation3 = SceneryAllocation( + scenery_id=self.scenery_id, scene_id=self.scene3_id + ) + session.add(allocation3) + + self.user_id = create_admin_user(session) + session.commit() + + self._app.digi_settings.settings["current_show"].set_value(self.show_id) + self.token = self._app.jwt_service.create_access_token( + data={"user_id": self.user_id} + ) + + # ========================================================================= + # GET Tests + # ========================================================================= + + def test_get_assignments_empty(self): + """Test GET with no assignments returns empty list.""" + response = self.fetch("/api/v1/show/stage/crew/assignments") + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("assignments", response_body) + self.assertEqual([], response_body["assignments"]) + + def test_get_assignments_returns_all(self): + """Test GET returns all assignments for the show.""" + # Create an assignment + with self._app.get_db().sessionmaker() as session: + assignment = CrewAssignment( + crew_id=self.crew_id, + scene_id=self.scene1_id, + assignment_type="set", + prop_id=self.prop_id, + ) + session.add(assignment) + session.commit() + + response = self.fetch("/api/v1/show/stage/crew/assignments") + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertEqual(1, len(response_body["assignments"])) + self.assertEqual(self.crew_id, response_body["assignments"][0]["crew_id"]) + self.assertEqual(self.scene1_id, response_body["assignments"][0]["scene_id"]) + self.assertEqual("set", response_body["assignments"][0]["assignment_type"]) + self.assertEqual(self.prop_id, response_body["assignments"][0]["prop_id"]) + + def test_get_assignments_no_show(self): + """Test GET returns 400 when no show is loaded.""" + self._app.digi_settings.settings["current_show"].set_value(None) + response = self.fetch("/api/v1/show/stage/crew/assignments") + self.assertEqual(400, response.code) + + # ========================================================================= + # POST Tests - Basic Creation + # ========================================================================= + + @gen_test + async def test_create_assignment_for_prop(self): + """Test POST creates a new crew assignment for a prop.""" + request = HTTPRequest( + self.get_url("/api/v1/show/stage/crew/assignments"), + method="POST", + body=tornado.escape.json_encode( + { + "crew_id": self.crew_id, + "scene_id": self.scene1_id, + "assignment_type": "set", + "prop_id": self.prop_id, + } + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request) + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("id", response_body) + self.assertIn("message", response_body) + + # Verify assignment was created + with self._app.get_db().sessionmaker() as session: + assignment = session.get(CrewAssignment, response_body["id"]) + self.assertIsNotNone(assignment) + self.assertEqual(self.crew_id, assignment.crew_id) + self.assertEqual(self.scene1_id, assignment.scene_id) + self.assertEqual("set", assignment.assignment_type) + self.assertEqual(self.prop_id, assignment.prop_id) + self.assertIsNone(assignment.scenery_id) + + @gen_test + async def test_create_assignment_for_scenery(self): + """Test POST creates a new crew assignment for scenery.""" + request = HTTPRequest( + self.get_url("/api/v1/show/stage/crew/assignments"), + method="POST", + body=tornado.escape.json_encode( + { + "crew_id": self.crew_id, + "scene_id": self.scene3_id, + "assignment_type": "set", + "scenery_id": self.scenery_id, + } + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request) + self.assertEqual(200, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("id", response_body) + + # Verify assignment was created + with self._app.get_db().sessionmaker() as session: + assignment = session.get(CrewAssignment, response_body["id"]) + self.assertIsNotNone(assignment) + self.assertIsNone(assignment.prop_id) + self.assertEqual(self.scenery_id, assignment.scenery_id) + + @gen_test + async def test_create_assignment_strike(self): + """Test POST creates a strike assignment.""" + request = HTTPRequest( + self.get_url("/api/v1/show/stage/crew/assignments"), + method="POST", + body=tornado.escape.json_encode( + { + "crew_id": self.crew_id, + "scene_id": self.scene2_id, # Valid STRIKE boundary + "assignment_type": "strike", + "prop_id": self.prop_id, + } + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request) + self.assertEqual(200, response.code) + + # ========================================================================= + # POST Tests - Validation Errors + # ========================================================================= + + @gen_test + async def test_create_assignment_missing_crew_id(self): + """Test POST returns 400 when crew_id is missing.""" + request = HTTPRequest( + self.get_url("/api/v1/show/stage/crew/assignments"), + method="POST", + body=tornado.escape.json_encode( + { + "scene_id": self.scene1_id, + "assignment_type": "set", + "prop_id": self.prop_id, + } + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request, raise_error=False) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("crew_id missing", response_body["message"]) + + @gen_test + async def test_create_assignment_missing_scene_id(self): + """Test POST returns 400 when scene_id is missing.""" + request = HTTPRequest( + self.get_url("/api/v1/show/stage/crew/assignments"), + method="POST", + body=tornado.escape.json_encode( + { + "crew_id": self.crew_id, + "assignment_type": "set", + "prop_id": self.prop_id, + } + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request, raise_error=False) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Scene ID missing", response_body["message"]) + + @gen_test + async def test_create_assignment_missing_assignment_type(self): + """Test POST returns 400 when assignment_type is missing.""" + request = HTTPRequest( + self.get_url("/api/v1/show/stage/crew/assignments"), + method="POST", + body=tornado.escape.json_encode( + { + "crew_id": self.crew_id, + "scene_id": self.scene1_id, + "prop_id": self.prop_id, + } + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request, raise_error=False) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("assignment_type missing", response_body["message"]) + + @gen_test + async def test_create_assignment_invalid_assignment_type(self): + """Test POST returns 400 for invalid assignment_type.""" + request = HTTPRequest( + self.get_url("/api/v1/show/stage/crew/assignments"), + method="POST", + body=tornado.escape.json_encode( + { + "crew_id": self.crew_id, + "scene_id": self.scene1_id, + "assignment_type": "invalid", + "prop_id": self.prop_id, + } + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request, raise_error=False) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("'set' or 'strike'", response_body["message"]) + + @gen_test + async def test_create_assignment_missing_item_id(self): + """Test POST returns 400 when neither prop_id nor scenery_id is provided.""" + request = HTTPRequest( + self.get_url("/api/v1/show/stage/crew/assignments"), + method="POST", + body=tornado.escape.json_encode( + { + "crew_id": self.crew_id, + "scene_id": self.scene1_id, + "assignment_type": "set", + } + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request, raise_error=False) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Either prop_id or scenery_id", response_body["message"]) + + @gen_test + async def test_create_assignment_both_item_ids(self): + """Test POST returns 400 when both prop_id and scenery_id are provided.""" + request = HTTPRequest( + self.get_url("/api/v1/show/stage/crew/assignments"), + method="POST", + body=tornado.escape.json_encode( + { + "crew_id": self.crew_id, + "scene_id": self.scene1_id, + "assignment_type": "set", + "prop_id": self.prop_id, + "scenery_id": self.scenery_id, + } + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request, raise_error=False) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Only one of prop_id or scenery_id", response_body["message"]) + + @gen_test + async def test_create_assignment_invalid_boundary(self): + """Test POST returns 400 for scene that is not a valid boundary.""" + # Scene 2 is valid STRIKE but not valid SET for the prop + request = HTTPRequest( + self.get_url("/api/v1/show/stage/crew/assignments"), + method="POST", + body=tornado.escape.json_encode( + { + "crew_id": self.crew_id, + "scene_id": self.scene2_id, # Not a valid SET boundary + "assignment_type": "set", + "prop_id": self.prop_id, + } + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request, raise_error=False) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("not a valid boundary", response_body["message"]) + + @gen_test + async def test_create_assignment_unallocated_scene(self): + """Test POST returns 400 for scene where item is not allocated.""" + # Scene 3 has no prop allocation + request = HTTPRequest( + self.get_url("/api/v1/show/stage/crew/assignments"), + method="POST", + body=tornado.escape.json_encode( + { + "crew_id": self.crew_id, + "scene_id": self.scene3_id, + "assignment_type": "set", + "prop_id": self.prop_id, + } + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request, raise_error=False) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("not a valid boundary", response_body["message"]) + + @gen_test + async def test_create_assignment_crew_not_found(self): + """Test POST returns 404 for non-existent crew member.""" + request = HTTPRequest( + self.get_url("/api/v1/show/stage/crew/assignments"), + method="POST", + body=tornado.escape.json_encode( + { + "crew_id": 99999, + "scene_id": self.scene1_id, + "assignment_type": "set", + "prop_id": self.prop_id, + } + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request, raise_error=False) + self.assertEqual(404, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("crew member not found", response_body["message"]) + + @gen_test + async def test_create_assignment_prop_not_found(self): + """Test POST returns 404 for non-existent prop.""" + request = HTTPRequest( + self.get_url("/api/v1/show/stage/crew/assignments"), + method="POST", + body=tornado.escape.json_encode( + { + "crew_id": self.crew_id, + "scene_id": self.scene1_id, + "assignment_type": "set", + "prop_id": 99999, + } + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request, raise_error=False) + self.assertEqual(404, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("prop not found", response_body["message"]) + + @gen_test + async def test_create_assignment_duplicate(self): + """Test POST returns 400 for duplicate assignment.""" + # Create first assignment + with self._app.get_db().sessionmaker() as session: + assignment = CrewAssignment( + crew_id=self.crew_id, + scene_id=self.scene1_id, + assignment_type="set", + prop_id=self.prop_id, + ) + session.add(assignment) + session.commit() + + # Try to create duplicate + request = HTTPRequest( + self.get_url("/api/v1/show/stage/crew/assignments"), + method="POST", + body=tornado.escape.json_encode( + { + "crew_id": self.crew_id, + "scene_id": self.scene1_id, + "assignment_type": "set", + "prop_id": self.prop_id, + } + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request, raise_error=False) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("already exists", response_body["message"]) + + # ========================================================================= + # PATCH Tests + # ========================================================================= + + @gen_test + async def test_update_assignment_crew_id(self): + """Test PATCH can update crew_id.""" + # Create a second crew member + with self._app.get_db().sessionmaker() as session: + crew2 = Crew(show_id=self.show_id, first_name="Jane", last_name="Smith") + session.add(crew2) + session.flush() + crew2_id = crew2.id + + # Create assignment + assignment = CrewAssignment( + crew_id=self.crew_id, + scene_id=self.scene1_id, + assignment_type="set", + prop_id=self.prop_id, + ) + session.add(assignment) + session.flush() + assignment_id = assignment.id + session.commit() + + request = HTTPRequest( + self.get_url("/api/v1/show/stage/crew/assignments"), + method="PATCH", + body=tornado.escape.json_encode({"id": assignment_id, "crew_id": crew2_id}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request) + self.assertEqual(200, response.code) + + # Verify update + with self._app.get_db().sessionmaker() as session: + assignment = session.get(CrewAssignment, assignment_id) + self.assertEqual(crew2_id, assignment.crew_id) + + @gen_test + async def test_update_assignment_assignment_type(self): + """Test PATCH can update assignment_type if new scene is valid boundary.""" + # Create assignment for SET at scene 1 + with self._app.get_db().sessionmaker() as session: + # Create single-scene allocation for scene 3 so it's valid for both + allocation = PropsAllocation(props_id=self.prop_id, scene_id=self.scene3_id) + session.add(allocation) + session.flush() + + assignment = CrewAssignment( + crew_id=self.crew_id, + scene_id=self.scene3_id, # Single-scene block - valid for both + assignment_type="set", + prop_id=self.prop_id, + ) + session.add(assignment) + session.flush() + assignment_id = assignment.id + session.commit() + + request = HTTPRequest( + self.get_url("/api/v1/show/stage/crew/assignments"), + method="PATCH", + body=tornado.escape.json_encode( + {"id": assignment_id, "assignment_type": "strike"} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request) + self.assertEqual(200, response.code) + + # Verify update + with self._app.get_db().sessionmaker() as session: + assignment = session.get(CrewAssignment, assignment_id) + self.assertEqual("strike", assignment.assignment_type) + + @gen_test + async def test_update_assignment_invalid_new_boundary(self): + """Test PATCH returns 400 when new combination is invalid boundary.""" + # Create assignment for SET at scene 1 + with self._app.get_db().sessionmaker() as session: + assignment = CrewAssignment( + crew_id=self.crew_id, + scene_id=self.scene1_id, + assignment_type="set", + prop_id=self.prop_id, + ) + session.add(assignment) + session.flush() + assignment_id = assignment.id + session.commit() + + # Try to change to STRIKE at scene 1 (not valid - scene 2 is STRIKE boundary) + request = HTTPRequest( + self.get_url("/api/v1/show/stage/crew/assignments"), + method="PATCH", + body=tornado.escape.json_encode( + {"id": assignment_id, "assignment_type": "strike"} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request, raise_error=False) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("not a valid boundary", response_body["message"]) + + @gen_test + async def test_update_assignment_not_found(self): + """Test PATCH returns 404 for non-existent assignment.""" + request = HTTPRequest( + self.get_url("/api/v1/show/stage/crew/assignments"), + method="PATCH", + body=tornado.escape.json_encode({"id": 99999, "crew_id": self.crew_id}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request, raise_error=False) + self.assertEqual(404, response.code) + + @gen_test + async def test_update_assignment_missing_id(self): + """Test PATCH returns 400 when id is missing.""" + request = HTTPRequest( + self.get_url("/api/v1/show/stage/crew/assignments"), + method="PATCH", + body=tornado.escape.json_encode({"crew_id": self.crew_id}), + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request, raise_error=False) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("ID missing", response_body["message"]) + + # ========================================================================= + # DELETE Tests + # ========================================================================= + + @gen_test + async def test_delete_assignment_success(self): + """Test DELETE removes an assignment.""" + with self._app.get_db().sessionmaker() as session: + assignment = CrewAssignment( + crew_id=self.crew_id, + scene_id=self.scene1_id, + assignment_type="set", + prop_id=self.prop_id, + ) + session.add(assignment) + session.flush() + assignment_id = assignment.id + session.commit() + + request = HTTPRequest( + self.get_url(f"/api/v1/show/stage/crew/assignments?id={assignment_id}"), + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request) + self.assertEqual(200, response.code) + + # Verify deletion + with self._app.get_db().sessionmaker() as session: + assignment = session.get(CrewAssignment, assignment_id) + self.assertIsNone(assignment) + + @gen_test + async def test_delete_assignment_not_found(self): + """Test DELETE returns 404 for non-existent assignment.""" + request = HTTPRequest( + self.get_url("/api/v1/show/stage/crew/assignments?id=99999"), + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request, raise_error=False) + self.assertEqual(404, response.code) + + @gen_test + async def test_delete_assignment_missing_id(self): + """Test DELETE returns 400 when ID is missing.""" + request = HTTPRequest( + self.get_url("/api/v1/show/stage/crew/assignments"), + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request, raise_error=False) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("ID missing", response_body["message"]) + + @gen_test + async def test_delete_assignment_invalid_id(self): + """Test DELETE returns 400 for non-integer ID.""" + request = HTTPRequest( + self.get_url("/api/v1/show/stage/crew/assignments?id=invalid"), + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request, raise_error=False) + self.assertEqual(400, response.code) + response_body = tornado.escape.json_decode(response.body) + self.assertIn("Invalid ID", response_body["message"]) + + +class TestCrewAssignmentCascadeDelete(DigiScriptTestCase): + """Test suite for CASCADE delete behavior on crew assignments.""" + + def setUp(self): + super().setUp() + with self._app.get_db().sessionmaker() as session: + self.show_id = create_show(session) + _, scene_ids = create_act_with_scenes( + session, + self.show_id, + "Act 1", + 1, + link_to_show=True, + ) + self.scene_id = scene_ids[0] + + self.crew_id = create_crew(session, self.show_id) + _, self.prop_id = create_prop(session, self.show_id) + + # Allocate prop to scene + allocation = PropsAllocation(props_id=self.prop_id, scene_id=self.scene_id) + session.add(allocation) + + # Create assignment + assignment = CrewAssignment( + crew_id=self.crew_id, + scene_id=self.scene_id, + assignment_type="set", + prop_id=self.prop_id, + ) + session.add(assignment) + session.flush() + self.assignment_id = assignment.id + + self.user_id = create_admin_user(session) + session.commit() + + self._app.digi_settings.settings["current_show"].set_value(self.show_id) + self.token = self._app.jwt_service.create_access_token( + data={"user_id": self.user_id} + ) + + def test_cascade_delete_on_crew_delete(self): + """Test assignment is deleted when crew member is deleted.""" + # Delete the crew member + with self._app.get_db().sessionmaker() as session: + crew = session.get(Crew, self.crew_id) + session.delete(crew) + session.commit() + + # Verify assignment was cascade deleted + assignment = session.get(CrewAssignment, self.assignment_id) + self.assertIsNone(assignment) + + def test_cascade_delete_on_prop_delete(self): + """Test assignment is deleted when prop is deleted.""" + # Delete the prop + with self._app.get_db().sessionmaker() as session: + prop = session.get(Props, self.prop_id) + session.delete(prop) + session.commit() + + # Verify assignment was cascade deleted + assignment = session.get(CrewAssignment, self.assignment_id) + self.assertIsNone(assignment) + + def test_cascade_delete_on_scenery_delete(self): + """Test assignment is deleted when scenery is deleted.""" + # Create a scenery item and assignment + with self._app.get_db().sessionmaker() as session: + _, scenery_id = create_scenery(session, self.show_id, name="Wall") + + # Allocate scenery to scene + allocation = SceneryAllocation( + scenery_id=scenery_id, scene_id=self.scene_id + ) + session.add(allocation) + + # Create assignment for scenery + assignment = CrewAssignment( + crew_id=self.crew_id, + scene_id=self.scene_id, + assignment_type="set", + scenery_id=scenery_id, + ) + session.add(assignment) + session.flush() + assignment_id = assignment.id + session.commit() + + # Delete the scenery + with self._app.get_db().sessionmaker() as session: + scenery = session.get(Scenery, scenery_id) + session.delete(scenery) + session.commit() + + # Verify assignment was cascade deleted + assignment = session.get(CrewAssignment, assignment_id) + self.assertIsNone(assignment) + + +class TestOrphanDeletionOnAllocationChange(DigiScriptTestCase): + """Test suite for orphan deletion when allocations change.""" + + def setUp(self): + super().setUp() + with self._app.get_db().sessionmaker() as session: + self.show_id = create_show(session) + _, scene_ids = create_act_with_scenes( + session, + self.show_id, + "Act 1", + 3, + link_to_show=True, + ) + self.scene1_id, self.scene2_id, self.scene3_id = scene_ids + + self.crew_id = create_crew(session, self.show_id) + _, self.prop_id = create_prop(session, self.show_id) + + # Allocate prop to scenes 1 and 2 (block: SET at 1, STRIKE at 2) + allocation1 = PropsAllocation( + props_id=self.prop_id, scene_id=self.scene1_id + ) + allocation2 = PropsAllocation( + props_id=self.prop_id, scene_id=self.scene2_id + ) + session.add_all([allocation1, allocation2]) + session.flush() + self.allocation2_id = allocation2.id + + # Create crew assignment at STRIKE boundary (scene 2) + assignment = CrewAssignment( + crew_id=self.crew_id, + scene_id=self.scene2_id, + assignment_type="strike", + prop_id=self.prop_id, + ) + session.add(assignment) + session.flush() + self.assignment_id = assignment.id + + self.user_id = create_admin_user(session) + session.commit() + + self._app.digi_settings.settings["current_show"].set_value(self.show_id) + self.token = self._app.jwt_service.create_access_token( + data={"user_id": self.user_id} + ) + + @gen_test + async def test_orphan_deleted_on_allocation_delete(self): + """Test crew assignment is deleted when allocation removal invalidates boundary.""" + # Delete allocation at scene 2 via API + # This should make scene 1 the new STRIKE boundary + # and orphan the assignment at scene 2 + request = HTTPRequest( + self.get_url( + f"/api/v1/show/stage/props/allocations?id={self.allocation2_id}" + ), + method="DELETE", + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request) + self.assertEqual(200, response.code) + + # Verify the crew assignment was orphan-deleted + with self._app.get_db().sessionmaker() as session: + assignment = session.get(CrewAssignment, self.assignment_id) + self.assertIsNone(assignment) + + @gen_test + async def test_orphan_deleted_on_allocation_create(self): + """Test crew assignment is deleted when new allocation changes boundaries.""" + # First, set up a scenario where adding allocation creates orphan + # Current: prop allocated to scenes 1,2 (SET:1, STRIKE:2) + # Add allocation to scene 3 (new block: scenes 1,2,3, SET:1, STRIKE:3) + # This makes scene 2 no longer a STRIKE boundary + + request = HTTPRequest( + self.get_url("/api/v1/show/stage/props/allocations"), + method="POST", + body=tornado.escape.json_encode( + {"props_id": self.prop_id, "scene_id": self.scene3_id} + ), + headers={"Authorization": f"Bearer {self.token}"}, + ) + response = await self.http_client.fetch(request) + self.assertEqual(200, response.code) + + # Verify the crew assignment at scene 2 STRIKE was orphan-deleted + with self._app.get_db().sessionmaker() as session: + assignment = session.get(CrewAssignment, self.assignment_id) + self.assertIsNone(assignment) diff --git a/server/test/controllers/api/show/stage/test_scenery.py b/server/test/controllers/api/show/stage/test_scenery.py index 5601ff20..1d08354d 100644 --- a/server/test/controllers/api/show/stage/test_scenery.py +++ b/server/test/controllers/api/show/stage/test_scenery.py @@ -719,9 +719,8 @@ def test_create_scenery_invalid_scenery_type_id(self): headers={"Authorization": f"Bearer {self.token}"}, ) self.assertEqual(400, response.code) - # Note: The controller has a typo in the error message response_body = tornado.escape.json_decode(response.body) - self.assertIn("Scenery prop type ID", response_body["message"]) + self.assertIn("Invalid scenery type ID", response_body["message"]) def test_create_scenery_scenery_type_not_found(self): """Test POST returns 404 for non-existent scenery type.""" @@ -922,9 +921,8 @@ def test_update_scenery_invalid_scenery_type_id(self): headers={"Authorization": f"Bearer {self.token}"}, ) self.assertEqual(400, response.code) - # Note: The controller has a typo in the error message response_body = tornado.escape.json_decode(response.body) - self.assertIn("Scenery prop type ID", response_body["message"]) + self.assertIn("Invalid scenery type ID", response_body["message"]) def test_update_scenery_scenery_type_not_found(self): """Test PATCH returns 404 for non-existent scenery type.""" diff --git a/server/test/helpers/__init__.py b/server/test/helpers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/test/helpers/stage_fixtures.py b/server/test/helpers/stage_fixtures.py new file mode 100644 index 00000000..cd8a4e8a --- /dev/null +++ b/server/test/helpers/stage_fixtures.py @@ -0,0 +1,160 @@ +"""Shared test fixture helpers for stage-related tests. + +These helpers reduce duplication across test setUp methods that need +Show / Act / Scene / Prop / Scenery / Crew / User scaffolding. +""" + +from models.show import Act, Scene, Show, ShowScriptType +from models.stage import Crew, Props, PropType, Scenery, SceneryType +from models.user import User + + +def create_show(session, name="Test Show"): + """Create a show and return its ID. + + :param session: SQLAlchemy session. + :param name: Show name. + :returns: The new show's ID. + :rtype: int + """ + show = Show(name=name, script_mode=ShowScriptType.FULL) + session.add(show) + session.flush() + return show.id + + +def create_act_with_scenes( + session, + show_id, + act_name, + num_scenes, + *, + interval_after=False, + previous_act_id=None, + link_to_show=False, +): + """Create an act with a linked-list of scenes. + + Scenes are named "Scene 1", "Scene 2", etc. and wired via + ``previous_scene_id``. The act's ``first_scene`` is set automatically. + + :param session: SQLAlchemy session. + :param show_id: Parent show ID. + :param act_name: Display name for the act. + :param num_scenes: Number of scenes to create. + :param interval_after: Whether the act has an interval after it. + :param previous_act_id: ID of the preceding act (for linked-list ordering). + :param link_to_show: If ``True``, sets ``show.first_act`` to this act. + :returns: Tuple of ``(act_id, [scene_id, ...])``. + :rtype: tuple[int, list[int]] + """ + act = Act( + show_id=show_id, + name=act_name, + interval_after=interval_after, + previous_act_id=previous_act_id, + ) + session.add(act) + session.flush() + + scene_ids = [] + previous_scene_id = None + for i in range(1, num_scenes + 1): + scene = Scene( + show_id=show_id, + act_id=act.id, + name=f"Scene {i}", + previous_scene_id=previous_scene_id, + ) + session.add(scene) + session.flush() + if i == 1: + act.first_scene = scene + previous_scene_id = scene.id + scene_ids.append(scene.id) + + if link_to_show: + show = session.get(Show, show_id) + show.first_act = act + + return act.id, scene_ids + + +def create_prop(session, show_id, name="Sword", type_name="Hand Props"): + """Create a prop type and prop, returning both IDs. + + :param session: SQLAlchemy session. + :param show_id: Parent show ID. + :param name: Prop name. + :param type_name: Prop type name. + :returns: Tuple of ``(prop_type_id, prop_id)``. + :rtype: tuple[int, int] + """ + prop_type = PropType(show_id=show_id, name=type_name, description="") + session.add(prop_type) + session.flush() + + prop = Props( + show_id=show_id, + prop_type_id=prop_type.id, + name=name, + description="", + ) + session.add(prop) + session.flush() + return prop_type.id, prop.id + + +def create_scenery(session, show_id, name="Castle Wall", type_name="Backdrops"): + """Create a scenery type and scenery item, returning both IDs. + + :param session: SQLAlchemy session. + :param show_id: Parent show ID. + :param name: Scenery name. + :param type_name: Scenery type name. + :returns: Tuple of ``(scenery_type_id, scenery_id)``. + :rtype: tuple[int, int] + """ + scenery_type = SceneryType(show_id=show_id, name=type_name, description="") + session.add(scenery_type) + session.flush() + + scenery = Scenery( + show_id=show_id, + scenery_type_id=scenery_type.id, + name=name, + description="", + ) + session.add(scenery) + session.flush() + return scenery_type.id, scenery.id + + +def create_crew(session, show_id, first_name="John", last_name="Doe"): + """Create a crew member and return the ID. + + :param session: SQLAlchemy session. + :param show_id: Parent show ID. + :param first_name: First name. + :param last_name: Last name. + :returns: The new crew member's ID. + :rtype: int + """ + crew = Crew(show_id=show_id, first_name=first_name, last_name=last_name) + session.add(crew) + session.flush() + return crew.id + + +def create_admin_user(session, username="admin"): + """Create an admin user and return the ID. + + :param session: SQLAlchemy session. + :param username: Username. + :returns: The new user's ID. + :rtype: int + """ + admin = User(username=username, is_admin=True, password="test") + session.add(admin) + session.flush() + return admin.id diff --git a/server/test/utils/show/test_block_computation.py b/server/test/utils/show/test_block_computation.py new file mode 100644 index 00000000..823bcd2d --- /dev/null +++ b/server/test/utils/show/test_block_computation.py @@ -0,0 +1,671 @@ +"""Unit tests for block computation utilities.""" + +from models.show import Scene, Show +from models.stage import ( + CrewAssignment, + Props, + PropsAllocation, + Scenery, + SceneryAllocation, +) +from test.conftest import DigiScriptTestCase +from test.helpers.stage_fixtures import ( + create_act_with_scenes, + create_crew, + create_prop, + create_scenery, + create_show, +) +from utils.show.block_computation import ( + Block, + compute_blocks_for_prop, + compute_blocks_for_scenery, + delete_orphaned_assignments_for_prop, + delete_orphaned_assignments_for_scenery, + find_orphaned_assignments_for_prop, + find_orphaned_assignments_for_scenery, + get_ordered_scenes_by_act, + is_valid_boundary, + is_valid_set_boundary, + is_valid_strike_boundary, +) + + +class TestGetOrderedScenesByAct(DigiScriptTestCase): + """Tests for get_ordered_scenes_by_act function.""" + + def setUp(self): + super().setUp() + with self._app.get_db().sessionmaker() as session: + self.show_id = create_show(session) + session.commit() + + def test_empty_show(self): + """Test with show that has no acts or scenes.""" + with self._app.get_db().sessionmaker() as session: + show = session.get(Show, self.show_id) + result = get_ordered_scenes_by_act(show) + self.assertEqual({}, result) + + def test_single_act_single_scene(self): + """Test with a single act containing a single scene.""" + with self._app.get_db().sessionmaker() as session: + act_id, scene_ids = create_act_with_scenes( + session, self.show_id, "Act 1", 1, link_to_show=True + ) + session.commit() + + show = session.get(Show, self.show_id) + result = get_ordered_scenes_by_act(show) + + self.assertEqual(1, len(result)) + self.assertIn(act_id, result) + self.assertEqual(1, len(result[act_id])) + self.assertEqual(scene_ids[0], result[act_id][0].id) + + def test_single_act_multiple_scenes(self): + """Test with a single act containing multiple scenes in linked list order.""" + with self._app.get_db().sessionmaker() as session: + act_id, scene_ids = create_act_with_scenes( + session, self.show_id, "Act 1", 3, link_to_show=True + ) + session.commit() + + show = session.get(Show, self.show_id) + result = get_ordered_scenes_by_act(show) + + self.assertEqual(1, len(result)) + self.assertEqual(3, len(result[act_id])) + self.assertEqual("Scene 1", result[act_id][0].name) + self.assertEqual("Scene 2", result[act_id][1].name) + self.assertEqual("Scene 3", result[act_id][2].name) + + def test_multiple_acts(self): + """Test with multiple acts each containing scenes.""" + with self._app.get_db().sessionmaker() as session: + act1_id, _ = create_act_with_scenes( + session, + self.show_id, + "Act 1", + 2, + interval_after=True, + link_to_show=True, + ) + act2_id, _ = create_act_with_scenes( + session, + self.show_id, + "Act 2", + 1, + previous_act_id=act1_id, + ) + session.commit() + + show = session.get(Show, self.show_id) + result = get_ordered_scenes_by_act(show) + + self.assertEqual(2, len(result)) + self.assertEqual(2, len(result[act1_id])) + self.assertEqual(1, len(result[act2_id])) + + +class TestComputeBlocksForProp(DigiScriptTestCase): + """Tests for compute_blocks_for_prop function.""" + + def setUp(self): + super().setUp() + with self._app.get_db().sessionmaker() as session: + self.show_id = create_show(session) + self.act1_id, scene_ids = create_act_with_scenes( + session, + self.show_id, + "Act 1", + 4, + interval_after=True, + link_to_show=True, + ) + self.scene1_id, self.scene2_id, self.scene3_id, self.scene4_id = scene_ids + _, self.prop_id = create_prop(session, self.show_id) + session.commit() + + def test_no_allocations(self): + """Test prop with no allocations returns empty list.""" + with self._app.get_db().sessionmaker() as session: + show = session.get(Show, self.show_id) + prop = session.get(Props, self.prop_id) + blocks = compute_blocks_for_prop(prop, show) + self.assertEqual([], blocks) + + def test_single_scene_allocation(self): + """Test single scene allocation creates one block.""" + with self._app.get_db().sessionmaker() as session: + # Allocate prop to scene 2 + allocation = PropsAllocation(props_id=self.prop_id, scene_id=self.scene2_id) + session.add(allocation) + session.commit() + + show = session.get(Show, self.show_id) + prop = session.get(Props, self.prop_id) + blocks = compute_blocks_for_prop(prop, show) + + self.assertEqual(1, len(blocks)) + self.assertEqual(self.act1_id, blocks[0].act_id) + self.assertEqual([self.scene2_id], blocks[0].scene_ids) + self.assertEqual(self.scene2_id, blocks[0].set_scene_id) + self.assertEqual(self.scene2_id, blocks[0].strike_scene_id) + self.assertTrue(blocks[0].is_single_scene) + + def test_consecutive_scenes(self): + """Test consecutive scene allocations form one block.""" + with self._app.get_db().sessionmaker() as session: + # Allocate prop to scenes 2 and 3 + allocation1 = PropsAllocation( + props_id=self.prop_id, scene_id=self.scene2_id + ) + allocation2 = PropsAllocation( + props_id=self.prop_id, scene_id=self.scene3_id + ) + session.add_all([allocation1, allocation2]) + session.commit() + + show = session.get(Show, self.show_id) + prop = session.get(Props, self.prop_id) + blocks = compute_blocks_for_prop(prop, show) + + self.assertEqual(1, len(blocks)) + self.assertEqual([self.scene2_id, self.scene3_id], blocks[0].scene_ids) + self.assertEqual(self.scene2_id, blocks[0].set_scene_id) + self.assertEqual(self.scene3_id, blocks[0].strike_scene_id) + self.assertFalse(blocks[0].is_single_scene) + + def test_gap_creates_separate_blocks(self): + """Test gap between allocations creates separate blocks.""" + with self._app.get_db().sessionmaker() as session: + # Allocate prop to scenes 1 and 3 (gap at scene 2) + allocation1 = PropsAllocation( + props_id=self.prop_id, scene_id=self.scene1_id + ) + allocation2 = PropsAllocation( + props_id=self.prop_id, scene_id=self.scene3_id + ) + session.add_all([allocation1, allocation2]) + session.commit() + + show = session.get(Show, self.show_id) + prop = session.get(Props, self.prop_id) + blocks = compute_blocks_for_prop(prop, show) + + self.assertEqual(2, len(blocks)) + + # First block: scene 1 + self.assertEqual([self.scene1_id], blocks[0].scene_ids) + self.assertEqual(self.scene1_id, blocks[0].set_scene_id) + self.assertEqual(self.scene1_id, blocks[0].strike_scene_id) + + # Second block: scene 3 + self.assertEqual([self.scene3_id], blocks[1].scene_ids) + self.assertEqual(self.scene3_id, blocks[1].set_scene_id) + self.assertEqual(self.scene3_id, blocks[1].strike_scene_id) + + def test_all_scenes_allocated(self): + """Test all scenes allocated forms one block.""" + with self._app.get_db().sessionmaker() as session: + for scene_id in [ + self.scene1_id, + self.scene2_id, + self.scene3_id, + self.scene4_id, + ]: + allocation = PropsAllocation(props_id=self.prop_id, scene_id=scene_id) + session.add(allocation) + session.commit() + + show = session.get(Show, self.show_id) + prop = session.get(Props, self.prop_id) + blocks = compute_blocks_for_prop(prop, show) + + self.assertEqual(1, len(blocks)) + self.assertEqual( + [self.scene1_id, self.scene2_id, self.scene3_id, self.scene4_id], + blocks[0].scene_ids, + ) + + def test_act_boundary_breaks_blocks(self): + """Test allocations in different acts create separate blocks.""" + with self._app.get_db().sessionmaker() as session: + # Create Act 2 with a scene + act2_id, act2_scene_ids = create_act_with_scenes( + session, + self.show_id, + "Act 2", + 1, + previous_act_id=self.act1_id, + ) + + # Allocate prop to last scene of Act 1 and first scene of Act 2 + allocation1 = PropsAllocation( + props_id=self.prop_id, scene_id=self.scene4_id + ) + allocation2 = PropsAllocation( + props_id=self.prop_id, scene_id=act2_scene_ids[0] + ) + session.add_all([allocation1, allocation2]) + session.commit() + + show = session.get(Show, self.show_id) + prop = session.get(Props, self.prop_id) + blocks = compute_blocks_for_prop(prop, show) + + # Should be 2 blocks, one per act + self.assertEqual(2, len(blocks)) + self.assertEqual(self.act1_id, blocks[0].act_id) + self.assertEqual(act2_id, blocks[1].act_id) + + +class TestComputeBlocksForScenery(DigiScriptTestCase): + """Tests for compute_blocks_for_scenery function.""" + + def setUp(self): + super().setUp() + with self._app.get_db().sessionmaker() as session: + self.show_id = create_show(session) + self.act1_id, scene_ids = create_act_with_scenes( + session, + self.show_id, + "Act 1", + 3, + link_to_show=True, + ) + self.scene1_id, self.scene2_id, self.scene3_id = scene_ids + _, self.scenery_id = create_scenery(session, self.show_id) + session.commit() + + def test_no_allocations(self): + """Test scenery with no allocations returns empty list.""" + with self._app.get_db().sessionmaker() as session: + show = session.get(Show, self.show_id) + scenery = session.get(Scenery, self.scenery_id) + blocks = compute_blocks_for_scenery(scenery, show) + self.assertEqual([], blocks) + + def test_consecutive_scenes(self): + """Test consecutive scene allocations for scenery form one block.""" + with self._app.get_db().sessionmaker() as session: + allocation1 = SceneryAllocation( + scenery_id=self.scenery_id, scene_id=self.scene1_id + ) + allocation2 = SceneryAllocation( + scenery_id=self.scenery_id, scene_id=self.scene2_id + ) + session.add_all([allocation1, allocation2]) + session.commit() + + show = session.get(Show, self.show_id) + scenery = session.get(Scenery, self.scenery_id) + blocks = compute_blocks_for_scenery(scenery, show) + + self.assertEqual(1, len(blocks)) + self.assertEqual([self.scene1_id, self.scene2_id], blocks[0].scene_ids) + self.assertEqual(self.scene1_id, blocks[0].set_scene_id) + self.assertEqual(self.scene2_id, blocks[0].strike_scene_id) + + +class TestBoundaryValidation(DigiScriptTestCase): + """Tests for boundary validation functions.""" + + def setUp(self): + super().setUp() + with self._app.get_db().sessionmaker() as session: + self.show_id = create_show(session) + self.act_id, scene_ids = create_act_with_scenes( + session, + self.show_id, + "Act 1", + 3, + link_to_show=True, + ) + self.scene1_id, self.scene2_id, self.scene3_id = scene_ids + _, self.prop_id = create_prop(session, self.show_id) + + # Allocate prop to scenes 1 and 2 (forms one block) + allocation1 = PropsAllocation( + props_id=self.prop_id, scene_id=self.scene1_id + ) + allocation2 = PropsAllocation( + props_id=self.prop_id, scene_id=self.scene2_id + ) + session.add_all([allocation1, allocation2]) + session.commit() + + def test_is_valid_set_boundary_first_scene(self): + """Test first scene of block is valid SET boundary.""" + with self._app.get_db().sessionmaker() as session: + show = session.get(Show, self.show_id) + result = is_valid_set_boundary( + session, self.scene1_id, self.prop_id, None, show + ) + self.assertTrue(result) + + def test_is_valid_set_boundary_middle_scene(self): + """Test middle scene of block is NOT valid SET boundary.""" + with self._app.get_db().sessionmaker() as session: + show = session.get(Show, self.show_id) + result = is_valid_set_boundary( + session, self.scene2_id, self.prop_id, None, show + ) + self.assertFalse(result) + + def test_is_valid_strike_boundary_last_scene(self): + """Test last scene of block is valid STRIKE boundary.""" + with self._app.get_db().sessionmaker() as session: + show = session.get(Show, self.show_id) + result = is_valid_strike_boundary( + session, self.scene2_id, self.prop_id, None, show + ) + self.assertTrue(result) + + def test_is_valid_strike_boundary_first_scene(self): + """Test first scene of multi-scene block is NOT valid STRIKE boundary.""" + with self._app.get_db().sessionmaker() as session: + show = session.get(Show, self.show_id) + result = is_valid_strike_boundary( + session, self.scene1_id, self.prop_id, None, show + ) + self.assertFalse(result) + + def test_is_valid_boundary_unallocated_scene(self): + """Test unallocated scene is not valid for any boundary type.""" + with self._app.get_db().sessionmaker() as session: + show = session.get(Show, self.show_id) + + # Scene 3 is not allocated + set_result = is_valid_set_boundary( + session, self.scene3_id, self.prop_id, None, show + ) + strike_result = is_valid_strike_boundary( + session, self.scene3_id, self.prop_id, None, show + ) + + self.assertFalse(set_result) + self.assertFalse(strike_result) + + def test_is_valid_boundary_single_scene_block(self): + """Test single-scene block is valid for both SET and STRIKE.""" + # Add a 4th scene and allocate to only scene 4 (gap at scene 3) + # This creates: Block 1 (scenes 1-2), gap at 3, Block 2 (scene 4 only) + with self._app.get_db().sessionmaker() as session: + # Add scene 4 + scene4 = Scene( + show_id=self.show_id, + act_id=self.act_id, + name="Scene 4", + previous_scene_id=self.scene3_id, + ) + session.add(scene4) + session.flush() + scene4_id = scene4.id + + # Allocate prop to scene 4 (gap at scene 3 creates separate block) + allocation = PropsAllocation(props_id=self.prop_id, scene_id=scene4_id) + session.add(allocation) + session.commit() + + # Use fresh session to ensure allocation is loaded + with self._app.get_db().sessionmaker() as session: + show = session.get(Show, self.show_id) + prop = session.get(Props, self.prop_id) + + # Verify the allocations exist in the fresh session + blocks = compute_blocks_for_prop(prop, show) + # Should have 2 blocks: scenes 1-2 and scene 4 + self.assertEqual(2, len(blocks)) + + # Scene 4 should be both SET and STRIKE for its single-scene block + set_result = is_valid_set_boundary( + session, scene4_id, self.prop_id, None, show + ) + strike_result = is_valid_strike_boundary( + session, scene4_id, self.prop_id, None, show + ) + + self.assertTrue(set_result) + self.assertTrue(strike_result) + + def test_is_valid_boundary_generic_function(self): + """Test is_valid_boundary dispatches correctly based on assignment_type.""" + with self._app.get_db().sessionmaker() as session: + show = session.get(Show, self.show_id) + + # Scene 1 is valid SET, not valid STRIKE + set_result = is_valid_boundary( + session, self.scene1_id, "set", self.prop_id, None, show + ) + strike_result = is_valid_boundary( + session, self.scene1_id, "strike", self.prop_id, None, show + ) + + self.assertTrue(set_result) + self.assertFalse(strike_result) + + # Scene 2 is valid STRIKE, not valid SET + set_result = is_valid_boundary( + session, self.scene2_id, "set", self.prop_id, None, show + ) + strike_result = is_valid_boundary( + session, self.scene2_id, "strike", self.prop_id, None, show + ) + + self.assertFalse(set_result) + self.assertTrue(strike_result) + + def test_is_valid_boundary_invalid_type(self): + """Test is_valid_boundary returns False for invalid assignment_type.""" + with self._app.get_db().sessionmaker() as session: + show = session.get(Show, self.show_id) + result = is_valid_boundary( + session, self.scene1_id, "invalid", self.prop_id, None, show + ) + self.assertFalse(result) + + def test_is_valid_boundary_nonexistent_prop(self): + """Test boundary check for non-existent prop returns False.""" + with self._app.get_db().sessionmaker() as session: + show = session.get(Show, self.show_id) + result = is_valid_boundary( + session, self.scene1_id, "set", 99999, None, show + ) + self.assertFalse(result) + + +class TestOrphanDetection(DigiScriptTestCase): + """Tests for orphaned crew assignment detection and deletion.""" + + def setUp(self): + super().setUp() + with self._app.get_db().sessionmaker() as session: + self.show_id = create_show(session) + self.act_id, scene_ids = create_act_with_scenes( + session, + self.show_id, + "Act 1", + 4, + link_to_show=True, + ) + ( + self.scene1_id, + self.scene2_id, + self.scene3_id, + self.scene4_id, + ) = scene_ids + self.crew_id = create_crew(session, self.show_id) + _, self.prop_id = create_prop(session, self.show_id) + + # Initial allocation: scenes 1, 2, 3 (one block) + # SET boundary: scene 1, STRIKE boundary: scene 3 + for scene_id in [self.scene1_id, self.scene2_id, self.scene3_id]: + allocation = PropsAllocation(props_id=self.prop_id, scene_id=scene_id) + session.add(allocation) + + session.commit() + + def test_find_orphaned_no_assignments(self): + """Test find_orphaned returns empty when no assignments exist.""" + with self._app.get_db().sessionmaker() as session: + show = session.get(Show, self.show_id) + prop = session.get(Props, self.prop_id) + orphaned = find_orphaned_assignments_for_prop(session, prop, show) + self.assertEqual([], orphaned) + + def test_find_orphaned_valid_assignments(self): + """Test find_orphaned returns empty for valid assignments.""" + with self._app.get_db().sessionmaker() as session: + # Create valid assignments + set_assignment = CrewAssignment( + crew_id=self.crew_id, + scene_id=self.scene1_id, # Valid SET boundary + assignment_type="set", + prop_id=self.prop_id, + ) + strike_assignment = CrewAssignment( + crew_id=self.crew_id, + scene_id=self.scene3_id, # Valid STRIKE boundary + assignment_type="strike", + prop_id=self.prop_id, + ) + session.add_all([set_assignment, strike_assignment]) + session.commit() + + show = session.get(Show, self.show_id) + prop = session.get(Props, self.prop_id) + orphaned = find_orphaned_assignments_for_prop(session, prop, show) + self.assertEqual([], orphaned) + + def test_find_orphaned_after_allocation_change(self): + """Test find_orphaned detects assignments that become invalid.""" + with self._app.get_db().sessionmaker() as session: + # Create assignment at scene 3 STRIKE + assignment = CrewAssignment( + crew_id=self.crew_id, + scene_id=self.scene3_id, + assignment_type="strike", + prop_id=self.prop_id, + ) + session.add(assignment) + session.commit() + assignment_id = assignment.id + + # Now remove scene 3 allocation (making scene 2 the new STRIKE boundary) + scene3_allocation = ( + session.query(PropsAllocation) + .filter_by(props_id=self.prop_id, scene_id=self.scene3_id) + .first() + ) + session.delete(scene3_allocation) + session.commit() + + show = session.get(Show, self.show_id) + prop = session.get(Props, self.prop_id) + session.refresh(prop) # Refresh to get updated allocations + orphaned = find_orphaned_assignments_for_prop(session, prop, show) + + self.assertEqual(1, len(orphaned)) + self.assertEqual(assignment_id, orphaned[0].id) + + def test_delete_orphaned_assignments(self): + """Test delete_orphaned actually removes orphaned assignments.""" + with self._app.get_db().sessionmaker() as session: + # Create assignment at scene 3 STRIKE + assignment = CrewAssignment( + crew_id=self.crew_id, + scene_id=self.scene3_id, + assignment_type="strike", + prop_id=self.prop_id, + ) + session.add(assignment) + session.commit() + assignment_id = assignment.id + + # Remove scene 3 allocation + scene3_allocation = ( + session.query(PropsAllocation) + .filter_by(props_id=self.prop_id, scene_id=self.scene3_id) + .first() + ) + session.delete(scene3_allocation) + session.commit() + + show = session.get(Show, self.show_id) + prop = session.get(Props, self.prop_id) + session.refresh(prop) + + deleted_ids = delete_orphaned_assignments_for_prop(session, prop, show) + session.commit() + + self.assertEqual([assignment_id], deleted_ids) + + # Verify assignment is deleted + assignment = session.get(CrewAssignment, assignment_id) + self.assertIsNone(assignment) + + def test_find_orphaned_for_scenery(self): + """Test orphan detection works for scenery items.""" + with self._app.get_db().sessionmaker() as session: + _, scenery_id = create_scenery(session, self.show_id) + + # Allocate to scenes 1 and 2 + allocation1 = SceneryAllocation( + scenery_id=scenery_id, scene_id=self.scene1_id + ) + allocation2 = SceneryAllocation( + scenery_id=scenery_id, scene_id=self.scene2_id + ) + session.add_all([allocation1, allocation2]) + + # Create assignment at scene 2 STRIKE + assignment = CrewAssignment( + crew_id=self.crew_id, + scene_id=self.scene2_id, + assignment_type="strike", + scenery_id=scenery_id, + ) + session.add(assignment) + session.commit() + assignment_id = assignment.id + + # Remove scene 2 allocation (making scene 1 both SET and STRIKE) + scene2_allocation = ( + session.query(SceneryAllocation) + .filter_by(scenery_id=scenery_id, scene_id=self.scene2_id) + .first() + ) + session.delete(scene2_allocation) + session.commit() + + show = session.get(Show, self.show_id) + scenery = session.get(Scenery, scenery_id) + session.refresh(scenery) + + orphaned = find_orphaned_assignments_for_scenery(session, scenery, show) + self.assertEqual(1, len(orphaned)) + self.assertEqual(assignment_id, orphaned[0].id) + + # Test deletion + deleted_ids = delete_orphaned_assignments_for_scenery( + session, scenery, show + ) + self.assertEqual([assignment_id], deleted_ids) + + +class TestBlockDataclass(DigiScriptTestCase): + """Tests for Block dataclass.""" + + def test_is_single_scene_true(self): + """Test is_single_scene returns True for single-scene blocks.""" + block = Block(act_id=1, scene_ids=[10], set_scene_id=10, strike_scene_id=10) + self.assertTrue(block.is_single_scene) + + def test_is_single_scene_false(self): + """Test is_single_scene returns False for multi-scene blocks.""" + block = Block( + act_id=1, scene_ids=[10, 11, 12], set_scene_id=10, strike_scene_id=12 + ) + self.assertFalse(block.is_single_scene) diff --git a/server/utils/pkg_utils.py b/server/utils/pkg_utils.py index c39937bc..3e4eaf13 100644 --- a/server/utils/pkg_utils.py +++ b/server/utils/pkg_utils.py @@ -27,6 +27,6 @@ def find_end_modules(path, prefix=None): modules = find_modules(path, prefix) end_modules = [] for module in modules: - if not any(x for x in modules if x != module and x.startswith(module)): + if not any(x for x in modules if x != module and x.startswith(module + ".")): end_modules.append(module) return end_modules diff --git a/server/utils/show/block_computation.py b/server/utils/show/block_computation.py new file mode 100644 index 00000000..a51feb03 --- /dev/null +++ b/server/utils/show/block_computation.py @@ -0,0 +1,346 @@ +""" +Utility functions for computing allocation blocks from scene-by-scene allocations. + +A "block" is a consecutive sequence of scenes where an item (prop or scenery) is allocated. +Blocks are computed per-act and never span act boundaries. + +For each block: +- The first scene is the SET boundary (where the item is brought on stage) +- The last scene is the STRIKE boundary (where the item is removed) +- Single-scene blocks have both SET and STRIKE on the same scene +""" + +from dataclasses import dataclass +from typing import List, Set + +from sqlalchemy import select +from sqlalchemy.orm import Session + +from models.show import Scene, Show +from models.stage import CrewAssignment, Props, Scenery + + +@dataclass +class Block: + """ + Represents a consecutive allocation block for an item within an act. + + :param act_id: ID of the act containing this block + :param scene_ids: List of scene IDs in this block, in order + :param set_scene_id: ID of the scene where the item is SET (first scene) + :param strike_scene_id: ID of the scene where the item is STRUCK (last scene) + """ + + act_id: int + scene_ids: List[int] + set_scene_id: int + strike_scene_id: int + + @property + def is_single_scene(self) -> bool: + """Return True if this block contains only one scene.""" + return len(self.scene_ids) == 1 + + +def get_ordered_scenes_by_act(show: Show) -> dict[int, List[Scene]]: + """ + Get all scenes in a show, grouped by act and ordered within each act. + + :param show: The show to get scenes for + :returns: Dictionary mapping act_id to list of scenes in order + """ + result: dict[int, List[Scene]] = {} + + # Traverse acts in order via linked list + act = show.first_act + while act: + scenes = [] + scene = act.first_scene + while scene: + scenes.append(scene) + scene = scene.next_scene + if scenes: + result[act.id] = scenes + act = act.next_act + + return result + + +def compute_blocks_for_prop(prop: Props, show: Show) -> List[Block]: + """ + Compute allocation blocks for a prop. + + :param prop: The prop to compute blocks for + :param show: The show containing the prop + :returns: List of Block objects representing consecutive allocations + """ + # Get all scene IDs where this prop is allocated + allocated_scene_ids: Set[int] = {alloc.scene_id for alloc in prop.scene_allocations} + + return _compute_blocks(show, allocated_scene_ids) + + +def compute_blocks_for_scenery(scenery: Scenery, show: Show) -> List[Block]: + """ + Compute allocation blocks for a scenery item. + + :param scenery: The scenery item to compute blocks for + :param show: The show containing the scenery + :returns: List of Block objects representing consecutive allocations + """ + # Get all scene IDs where this scenery is allocated + allocated_scene_ids: Set[int] = { + alloc.scene_id for alloc in scenery.scene_allocations + } + + return _compute_blocks(show, allocated_scene_ids) + + +def _compute_blocks(show: Show, allocated_scene_ids: Set[int]) -> List[Block]: + """ + Internal function to compute blocks from a set of allocated scene IDs. + + :param show: The show to compute blocks for + :param allocated_scene_ids: Set of scene IDs where the item is allocated + :returns: List of Block objects + """ + if not allocated_scene_ids: + return [] + + blocks: List[Block] = [] + ordered_scenes_by_act = get_ordered_scenes_by_act(show) + + for act_id, scenes in ordered_scenes_by_act.items(): + current_block_scenes: List[int] = [] + + for scene in scenes: + if scene.id in allocated_scene_ids: + # Scene is allocated - add to current block + current_block_scenes.append(scene.id) + # Scene is not allocated - end current block if one exists + elif current_block_scenes: + blocks.append( + Block( + act_id=act_id, + scene_ids=current_block_scenes.copy(), + set_scene_id=current_block_scenes[0], + strike_scene_id=current_block_scenes[-1], + ) + ) + current_block_scenes = [] + + # Don't forget the last block in the act + if current_block_scenes: + blocks.append( + Block( + act_id=act_id, + scene_ids=current_block_scenes.copy(), + set_scene_id=current_block_scenes[0], + strike_scene_id=current_block_scenes[-1], + ) + ) + + return blocks + + +def is_valid_set_boundary( + session: Session, + scene_id: int, + prop_id: int | None, + scenery_id: int | None, + show: Show, +) -> bool: + """ + Check if a scene is a valid SET boundary for an item. + + A scene is a valid SET boundary if it's the first scene of a block + (i.e., the item is allocated to this scene and either it's the first + scene in the act or the previous scene doesn't have the item allocated). + + :param session: Database session + :param scene_id: Scene ID to check + :param prop_id: Prop ID (if checking a prop) + :param scenery_id: Scenery ID (if checking scenery) + :param show: The show + :returns: True if the scene is a valid SET boundary + """ + if prop_id is not None: + prop = session.get(Props, prop_id) + if not prop: + return False + blocks = compute_blocks_for_prop(prop, show) + elif scenery_id is not None: + scenery = session.get(Scenery, scenery_id) + if not scenery: + return False + blocks = compute_blocks_for_scenery(scenery, show) + else: + return False + + return any(block.set_scene_id == scene_id for block in blocks) + + +def is_valid_strike_boundary( + session: Session, + scene_id: int, + prop_id: int | None, + scenery_id: int | None, + show: Show, +) -> bool: + """ + Check if a scene is a valid STRIKE boundary for an item. + + A scene is a valid STRIKE boundary if it's the last scene of a block. + + :param session: Database session + :param scene_id: Scene ID to check + :param prop_id: Prop ID (if checking a prop) + :param scenery_id: Scenery ID (if checking scenery) + :param show: The show + :returns: True if the scene is a valid STRIKE boundary + """ + if prop_id is not None: + prop = session.get(Props, prop_id) + if not prop: + return False + blocks = compute_blocks_for_prop(prop, show) + elif scenery_id is not None: + scenery = session.get(Scenery, scenery_id) + if not scenery: + return False + blocks = compute_blocks_for_scenery(scenery, show) + else: + return False + + return any(block.strike_scene_id == scene_id for block in blocks) + + +def is_valid_boundary( + session: Session, + scene_id: int, + assignment_type: str, + prop_id: int | None, + scenery_id: int | None, + show: Show, +) -> bool: + """ + Check if a scene is a valid boundary for a crew assignment. + + :param session: Database session + :param scene_id: Scene ID to check + :param assignment_type: 'set' or 'strike' + :param prop_id: Prop ID (if checking a prop) + :param scenery_id: Scenery ID (if checking scenery) + :param show: The show + :returns: True if the scene is a valid boundary for the assignment type + """ + if assignment_type == "set": + return is_valid_set_boundary(session, scene_id, prop_id, scenery_id, show) + elif assignment_type == "strike": + return is_valid_strike_boundary(session, scene_id, prop_id, scenery_id, show) + else: + return False + + +def find_orphaned_assignments_for_prop( + session: Session, prop: Props, show: Show +) -> List[CrewAssignment]: + """ + Find crew assignments for a prop that are no longer on valid block boundaries. + + :param session: Database session + :param prop: The prop to check + :param show: The show + :returns: List of orphaned CrewAssignment objects + """ + blocks = compute_blocks_for_prop(prop, show) + valid_set_scenes = {block.set_scene_id for block in blocks} + valid_strike_scenes = {block.strike_scene_id for block in blocks} + + # Get all crew assignments for this prop + assignments = session.scalars( + select(CrewAssignment).where(CrewAssignment.prop_id == prop.id) + ).all() + + orphaned = [] + for assignment in assignments: + if assignment.assignment_type == "set": + if assignment.scene_id not in valid_set_scenes: + orphaned.append(assignment) + elif assignment.assignment_type == "strike": + if assignment.scene_id not in valid_strike_scenes: + orphaned.append(assignment) + + return orphaned + + +def find_orphaned_assignments_for_scenery( + session: Session, scenery: Scenery, show: Show +) -> List[CrewAssignment]: + """ + Find crew assignments for a scenery item that are no longer on valid block boundaries. + + :param session: Database session + :param scenery: The scenery item to check + :param show: The show + :returns: List of orphaned CrewAssignment objects + """ + blocks = compute_blocks_for_scenery(scenery, show) + valid_set_scenes = {block.set_scene_id for block in blocks} + valid_strike_scenes = {block.strike_scene_id for block in blocks} + + # Get all crew assignments for this scenery + assignments = session.scalars( + select(CrewAssignment).where(CrewAssignment.scenery_id == scenery.id) + ).all() + + orphaned = [] + for assignment in assignments: + if assignment.assignment_type == "set": + if assignment.scene_id not in valid_set_scenes: + orphaned.append(assignment) + elif assignment.assignment_type == "strike": + if assignment.scene_id not in valid_strike_scenes: + orphaned.append(assignment) + + return orphaned + + +def delete_orphaned_assignments_for_prop( + session: Session, prop: Props, show: Show +) -> List[int]: + """ + Delete crew assignments for a prop that are no longer on valid block boundaries. + + :param session: Database session + :param prop: The prop to check + :param show: The show + :returns: List of IDs of deleted assignments + """ + orphaned = find_orphaned_assignments_for_prop(session, prop, show) + deleted_ids = [a.id for a in orphaned] + + for assignment in orphaned: + session.delete(assignment) + + return deleted_ids + + +def delete_orphaned_assignments_for_scenery( + session: Session, scenery: Scenery, show: Show +) -> List[int]: + """ + Delete crew assignments for a scenery item that are no longer on valid block boundaries. + + :param session: Database session + :param scenery: The scenery item to check + :param show: The show + :returns: List of IDs of deleted assignments + """ + orphaned = find_orphaned_assignments_for_scenery(session, scenery, show) + deleted_ids = [a.id for a in orphaned] + + for assignment in orphaned: + session.delete(assignment) + + return deleted_ids From 0b04581198a74f9d0f3a465899ad529582a0c1c5 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Wed, 11 Feb 2026 12:24:33 +0000 Subject: [PATCH 21/43] [npm] Update packages --- electron/package-lock.json | 16 ++++++++-------- electron/package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/electron/package-lock.json b/electron/package-lock.json index f660872d..4a94dd34 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -19,11 +19,11 @@ "@electron-forge/maker-squirrel": "^7.11.1", "@electron-forge/maker-zip": "^7.11.1", "@eslint/js": "^9.39.2", - "electron": "^40.1.0", + "electron": "^40.3.0", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.5", - "globals": "^17.2.0", + "globals": "^17.3.0", "prettier": "^3.8.1" }, "engines": { @@ -2869,9 +2869,9 @@ "license": "MIT" }, "node_modules/electron": { - "version": "40.1.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-40.1.0.tgz", - "integrity": "sha512-2j/kvw7uF0H1PnzYBzw2k2Q6q16J8ToKrtQzZfsAoXbbMY0l5gQi2DLOauIZLzwp4O01n8Wt/74JhSRwG0yj9A==", + "version": "40.3.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-40.3.0.tgz", + "integrity": "sha512-ZaDkTZpNHr863tyZHieoqbaiLI0e3RVCXoEC5y1Ld70/Q5H1mPV9d5TK0h1dWtaSFVOW0w8iDvtdLwAXtasXpg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4452,9 +4452,9 @@ } }, "node_modules/globals": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.2.0.tgz", - "integrity": "sha512-tovnCz/fEq+Ripoq+p/gN1u7l6A7wwkoBT9pRCzTHzsD/LvADIzXZdjmRymh5Ztf0DYC3Rwg5cZRYjxzBmzbWg==", + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", + "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", "dev": true, "license": "MIT", "engines": { diff --git a/electron/package.json b/electron/package.json index 6a48688e..83ea6b4e 100644 --- a/electron/package.json +++ b/electron/package.json @@ -29,7 +29,7 @@ "bonjour-service": "^1.3.0" }, "devDependencies": { - "electron": "^40.1.0", + "electron": "^40.3.0", "@electron-forge/cli": "^7.11.1", "@electron-forge/maker-squirrel": "^7.11.1", "@electron-forge/maker-zip": "^7.11.1", @@ -39,7 +39,7 @@ "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.5", - "globals": "^17.2.0", + "globals": "^17.3.0", "prettier": "^3.8.1" }, "config": { From c8cefdf74d1f2056f7b93ff1893fba386bd98bdc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 12:29:05 +0000 Subject: [PATCH 22/43] Bump alembic from 1.18.3 to 1.18.4 in /server (#903) Bumps [alembic](https://github.com/sqlalchemy/alembic) from 1.18.3 to 1.18.4. - [Release notes](https://github.com/sqlalchemy/alembic/releases) - [Changelog](https://github.com/sqlalchemy/alembic/blob/main/CHANGES) - [Commits](https://github.com/sqlalchemy/alembic/commits) --- updated-dependencies: - dependency-name: alembic dependency-version: 1.18.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- server/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/requirements.txt b/server/requirements.txt index 632d64d0..cb7fec8a 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -6,7 +6,7 @@ marshmallow-sqlalchemy>=1.4.0 tornado-prometheus==0.1.2 bcrypt==4.3.0 anytree==2.13.0 -alembic==1.18.3 +alembic==1.18.4 marshmallow<5 pyjwt[crypto]==2.11.0 setuptools==80.10.2 From 697f892c0c5aa7e0eda2e98b2e25bbe5b9c599b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 12:50:39 +0000 Subject: [PATCH 23/43] Bump ruff from 0.14.14 to 0.15.0 in /server (#899) * Bump ruff from 0.14.14 to 0.15.0 in /server Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.14 to 0.15.0. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.14.14...0.15.0) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.15.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Fix ruff 0.15.0 lint and formatting violations Address newly stabilized rules and 2026 formatter style: - PLW0108: Add noqa for Marshmallow forward-reference lambda in schemas.py - PLC0207: Add maxsplit=1 to str.split() in version_checker.py - Parenthesize multi-line ternary in lambda for 2026 format style Co-Authored-By: Claude Opus 4.6 --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tim Bradgate Co-authored-by: Claude Opus 4.6 --- server/controllers/api/show/microphones.py | 8 +++++--- server/schemas/schemas.py | 2 +- server/test_requirements.txt | 2 +- server/utils/version_checker.py | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/server/controllers/api/show/microphones.py b/server/controllers/api/show/microphones.py index ad94a8b5..5b07429e 100644 --- a/server/controllers/api/show/microphones.py +++ b/server/controllers/api/show/microphones.py @@ -498,9 +498,11 @@ async def post(self): ] # Sort by scene position (chronological order) character_scenes.sort( - key=lambda x: scene_metadata[x[0]].position - if x[0] in scene_metadata - else 0 + key=lambda x: ( + scene_metadata[x[0]].position + if x[0] in scene_metadata + else 0 + ) ) # Assign mic for each scene diff --git a/server/schemas/schemas.py b/server/schemas/schemas.py index cee93003..9feacdb7 100644 --- a/server/schemas/schemas.py +++ b/server/schemas/schemas.py @@ -227,7 +227,7 @@ class Meta: include_fk = True line_parts = Nested( - lambda: ScriptLinePartSchema(), + lambda: ScriptLinePartSchema(), # noqa: PLW0108 — forward reference required many=True, ) diff --git a/server/test_requirements.txt b/server/test_requirements.txt index 0f664215..afa631cb 100644 --- a/server/test_requirements.txt +++ b/server/test_requirements.txt @@ -1,3 +1,3 @@ pytest<9.1 pytest-asyncio>=1.3.0 -ruff==0.14.14 \ No newline at end of file +ruff==0.15.0 \ No newline at end of file diff --git a/server/utils/version_checker.py b/server/utils/version_checker.py index f5488195..cd330857 100644 --- a/server/utils/version_checker.py +++ b/server/utils/version_checker.py @@ -198,8 +198,8 @@ def _is_newer_version(self, latest: str, current: str) -> bool: """ try: # Strip pre-release suffixes (everything after -) - latest_clean = latest.split("-")[0] - current_clean = current.split("-")[0] + latest_clean = latest.split("-", maxsplit=1)[0] + current_clean = current.split("-", maxsplit=1)[0] latest_parts = [int(x) for x in latest_clean.split(".")] current_parts = [int(x) for x in current_clean.split(".")] From 5d8f71722aaf530dc6084edeb3c74330e3bbe1d2 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Fri, 13 Feb 2026 16:36:10 +0000 Subject: [PATCH 24/43] [npm] Update electron packages --- electron/package-lock.json | 8 ++++---- electron/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/electron/package-lock.json b/electron/package-lock.json index 4a94dd34..8ad59fa3 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -19,7 +19,7 @@ "@electron-forge/maker-squirrel": "^7.11.1", "@electron-forge/maker-zip": "^7.11.1", "@eslint/js": "^9.39.2", - "electron": "^40.3.0", + "electron": "^40.4.0", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.5", @@ -2869,9 +2869,9 @@ "license": "MIT" }, "node_modules/electron": { - "version": "40.3.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-40.3.0.tgz", - "integrity": "sha512-ZaDkTZpNHr863tyZHieoqbaiLI0e3RVCXoEC5y1Ld70/Q5H1mPV9d5TK0h1dWtaSFVOW0w8iDvtdLwAXtasXpg==", + "version": "40.4.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-40.4.0.tgz", + "integrity": "sha512-31l4V7Ys4oUuXyaN/cCNnyBdDXN9RwOVOG+JhiHCf4zx5tZkHd43PKGY6KLEWpeYCxaphsuGSEjagJLfPqKj8g==", "dev": true, "hasInstallScript": true, "license": "MIT", diff --git a/electron/package.json b/electron/package.json index 83ea6b4e..959549f5 100644 --- a/electron/package.json +++ b/electron/package.json @@ -29,7 +29,7 @@ "bonjour-service": "^1.3.0" }, "devDependencies": { - "electron": "^40.3.0", + "electron": "^40.4.0", "@electron-forge/cli": "^7.11.1", "@electron-forge/maker-squirrel": "^7.11.1", "@electron-forge/maker-zip": "^7.11.1", From e7bd11617c85a764b99c05e5c8a3c462bfefe0e0 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Fri, 13 Feb 2026 16:38:38 +0000 Subject: [PATCH 25/43] [pypi] Bump ruff to 0.15.1 --- server/test_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/test_requirements.txt b/server/test_requirements.txt index afa631cb..cf8a867c 100644 --- a/server/test_requirements.txt +++ b/server/test_requirements.txt @@ -1,3 +1,3 @@ pytest<9.1 pytest-asyncio>=1.3.0 -ruff==0.15.0 \ No newline at end of file +ruff==0.15.1 \ No newline at end of file From e351e071af9b72f8542e4483881977adbe326d5d Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Fri, 13 Feb 2026 18:07:07 +0000 Subject: [PATCH 26/43] Allow user configurable log level (#911) 1. Allow users to select log level from settings UI dropdown 2. Fix DB logging config to remove handler when turned off --- .../vue_components/config/ConfigSettings.vue | 27 ++++++++- server/digi_server/app_server.py | 35 ++++++++---- server/digi_server/logger.py | 47 +++++++++++++--- server/digi_server/settings.py | 55 +++++++++++++++++-- 4 files changed, 138 insertions(+), 26 deletions(-) diff --git a/client/src/vue_components/config/ConfigSettings.vue b/client/src/vue_components/config/ConfigSettings.vue index 2fec9230..fbf7f9af 100644 --- a/client/src/vue_components/config/ConfigSettings.vue +++ b/client/src/vue_components/config/ConfigSettings.vue @@ -32,8 +32,18 @@