diff --git a/.coveragerc b/.coveragerc
index b1c1c93c3598..0181948d2bf9 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -19,4 +19,3 @@ omit =
src/dispatch/api.py
src/dispatch/extensions.py
src/dispatch/scheduler.py
-
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 37d7474cb362..69a57adfa98e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -7,15 +7,31 @@
# To Skip Checks:
#
# git commit --no-verify
+# git push --no-verify
+# test
+#
+# To update all hooks automatically:
+#
+# pre-commit autoupdate
fail_fast: false
default_language_version:
python: python3.11.2
repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v5.0.0
+ hooks:
+ - id: no-commit-to-branch # prevent direct commits to the `main` branch
+ - id: check-yaml
+ args: ["--unsafe"]
+ - id: check-toml
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+
- repo: https://github.com/astral-sh/ruff-pre-commit
# ruff version.
- rev: v0.7.0
+ rev: v0.11.12
hooks:
# Run the linter.
#
@@ -26,19 +42,27 @@ repos:
# Run the formatter.
- id: ruff-format
- # Typos
+ # typos
- repo: https://github.com/crate-ci/typos
- rev: v1.26.1
+ rev: v1.33.1
hooks:
- id: typos
- exclude: ^(data/dispatch-sample-data.dump|src/dispatch/static/dispatch/src/|src/dispatch/database/revisions/)
+ exclude: |
+ (?x)^(
+ src/dispatch/database/revisions/core/versions/|
+ src/dispatch/database/revisions/tenant/versions/|
+ src/dispatch/static/dispatch/src/assets/icons/|
+ data/dispatch-sample-data.dump|
+ src/dispatch/plugins/dispatch_zoom/client.py
+ )
- # Pytest
+ # pytest
- repo: local
hooks:
- id: tests
name: run tests
- entry: pytest -v tests/
+ entry: pytest -v tests
language: system
- types: [python]
- stages: [push]
+ pass_filenames: false
+ always_run: true
+ stages: [pre-push]
diff --git a/docker/Dockerfile b/docker/Dockerfile
index fcab6e531578..580ef3c5d644 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -106,7 +106,7 @@ RUN buildDeps="" \
&& rm -rf /var/lib/apt/lists/* \
# mjml has to be installed differently here because
# after node 14, docker will install npm files at the
- # root directoy and fail, so we have to create a new
+ # root directory and fail, so we have to create a new
# directory and use it for the install then copy the
# files to the root directory to maintain backwards
# compatibility for email generation
diff --git a/src/dispatch/auth/service.py b/src/dispatch/auth/service.py
index a965222b6d83..fe5b0bd597bf 100644
--- a/src/dispatch/auth/service.py
+++ b/src/dispatch/auth/service.py
@@ -152,7 +152,8 @@ def create(*, db_session, organization: str, user_in: (UserRegister | UserCreate
# create the user
user = DispatchUser(
- **user_in.model_dump(exclude={"password", "organizations", "projects", "role"}), password=password
+ **user_in.model_dump(exclude={"password", "organizations", "projects", "role"}),
+ password=password,
)
org = organization_service.get_by_slug_or_raise(
diff --git a/src/dispatch/case/flows.py b/src/dispatch/case/flows.py
index 38381bd26e4c..304912a2fa39 100644
--- a/src/dispatch/case/flows.py
+++ b/src/dispatch/case/flows.py
@@ -42,7 +42,7 @@
send_case_rating_feedback_message,
send_case_update_notifications,
send_event_paging_message,
- send_event_update_prompt_reminder
+ send_event_update_prompt_reminder,
)
from .models import Case
from .service import get
diff --git a/src/dispatch/case/messaging.py b/src/dispatch/case/messaging.py
index 8242614ad531..12dd1136d5dc 100644
--- a/src/dispatch/case/messaging.py
+++ b/src/dispatch/case/messaging.py
@@ -421,7 +421,7 @@ def send_event_update_prompt_reminder(case: Case, db_session: Session) -> None:
"text": {"type": "plain_text", "text": "Update Case"},
"action_id": CaseNotificationActions.update,
"style": "primary",
- "value": button_metadata
+ "value": button_metadata,
}
],
},
@@ -430,6 +430,7 @@ def send_event_update_prompt_reminder(case: Case, db_session: Session) -> None:
log.debug(f"Security Event update reminder sent to {case.assignee.individual.email}.")
+
def send_event_paging_message(case: Case, db_session: Session, oncall_name: str) -> None:
"""
Sends a message to the case conversation channel to notify the reporter that they can engage
diff --git a/src/dispatch/case/models.py b/src/dispatch/case/models.py
index 0254f17a0b46..4315ab3088e2 100644
--- a/src/dispatch/case/models.py
+++ b/src/dispatch/case/models.py
@@ -336,6 +336,7 @@ class CaseReadMinimal(CaseBase):
assignee: ParticipantReadMinimal | None = None
case_costs: list[CaseCostReadMinimal] = []
+
class CaseReadMinimalWithExtras(CaseBase):
"""Pydantic model for reading minimal case data."""
@@ -451,11 +452,13 @@ class CasePagination(Pagination):
items: list[CaseReadMinimal] = []
+
class CasePaginationMinimalWithExtras(Pagination):
"""Pydantic model for paginated minimal case results."""
items: list[CaseReadMinimalWithExtras] = []
+
class CaseExpandedPagination(Pagination):
"""Pydantic model for paginated expanded case results."""
diff --git a/src/dispatch/case/service.py b/src/dispatch/case/service.py
index e2ce5ca40ed9..da4a623c2c93 100644
--- a/src/dispatch/case/service.py
+++ b/src/dispatch/case/service.py
@@ -375,8 +375,7 @@ def update(*, db_session, case: Case, case_in: CaseUpdate, current_user: Dispatc
db_session=db_session,
source="Dispatch Core App",
description=(
- f"Case visibility changed to {case_in.visibility.lower()} "
- f"by {current_user.email}"
+ f"Case visibility changed to {case_in.visibility.lower()} by {current_user.email}"
),
dispatch_user_id=current_user.id,
case_id=case.id,
diff --git a/src/dispatch/case/type/models.py b/src/dispatch/case/type/models.py
index 9e6db2b12229..9c0746f4dde4 100644
--- a/src/dispatch/case/type/models.py
+++ b/src/dispatch/case/type/models.py
@@ -1,4 +1,5 @@
"""Models for case types and related entities in the Dispatch application."""
+
from pydantic import field_validator, AnyHttpUrl
from sqlalchemy import JSON, Boolean, Column, ForeignKey, Integer, String
@@ -19,6 +20,7 @@
class CaseType(ProjectMixin, Base):
"""SQLAlchemy model for case types, representing different types of cases in the system."""
+
__table_args__ = (UniqueConstraint("name", "project_id"),)
id = Column(Integer, primary_key=True)
name = Column(String)
@@ -66,6 +68,7 @@ def get_meta(self, slug):
# Pydantic models
class Document(DispatchBase):
"""Pydantic model for a document related to a case type."""
+
id: PrimaryKey
description: str | None = None
name: NameStr
@@ -76,6 +79,7 @@ class Document(DispatchBase):
class IncidentType(DispatchBase):
"""Pydantic model for an incident type related to a case type."""
+
id: PrimaryKey
description: str | None = None
name: NameStr
@@ -84,6 +88,7 @@ class IncidentType(DispatchBase):
class Service(DispatchBase):
"""Pydantic model for a service related to a case type."""
+
id: PrimaryKey
description: str | None = None
external_id: str
@@ -94,6 +99,7 @@ class Service(DispatchBase):
class CaseTypeBase(DispatchBase):
"""Base Pydantic model for case types, used for shared fields."""
+
case_template_document: Document | None = None
conversation_target: str | None = None
default: bool | None = False
@@ -118,19 +124,23 @@ def replace_none_with_empty_list(cls, value):
class CaseTypeCreate(CaseTypeBase):
"""Pydantic model for creating a new case type."""
+
pass
class CaseTypeUpdate(CaseTypeBase):
"""Pydantic model for updating an existing case type."""
+
id: PrimaryKey | None = None
class CaseTypeRead(CaseTypeBase):
"""Pydantic model for reading a case type from the database."""
+
id: PrimaryKey
class CaseTypePagination(Pagination):
"""Pydantic model for paginated case type results."""
+
items: list[CaseTypeRead] = []
diff --git a/src/dispatch/case/views.py b/src/dispatch/case/views.py
index 7b57456de8cb..e23b89131833 100644
--- a/src/dispatch/case/views.py
+++ b/src/dispatch/case/views.py
@@ -39,7 +39,15 @@
case_update_flow,
get_case_participants_flow,
)
-from .models import Case, CaseCreate, CaseExpandedPagination, CasePagination, CasePaginationMinimalWithExtras, CaseRead, CaseUpdate
+from .models import (
+ Case,
+ CaseCreate,
+ CaseExpandedPagination,
+ CasePagination,
+ CasePaginationMinimalWithExtras,
+ CaseRead,
+ CaseUpdate,
+)
from .service import create, delete, get, get_participants, update
log = logging.getLogger(__name__)
@@ -144,6 +152,7 @@ def get_cases_minimal(
return json.loads(CasePaginationMinimalWithExtras(**pagination).json())
+
@router.post("", response_model=CaseRead, summary="Creates a new case.")
def create_case(
db_session: DbSession,
diff --git a/src/dispatch/case_cost_type/service.py b/src/dispatch/case_cost_type/service.py
index 96fd2f278925..5de43d56f5cb 100644
--- a/src/dispatch/case_cost_type/service.py
+++ b/src/dispatch/case_cost_type/service.py
@@ -16,9 +16,7 @@ def get(*, db_session, case_cost_type_id: int) -> CaseCostType | None:
return db_session.query(CaseCostType).filter(CaseCostType.id == case_cost_type_id).one_or_none()
-def get_response_cost_type(
- *, db_session, project_id: int, model_type: str
-) -> CaseCostType | None:
+def get_response_cost_type(*, db_session, project_id: int, model_type: str) -> CaseCostType | None:
"""Gets the default response cost type."""
return (
db_session.query(CaseCostType)
@@ -52,9 +50,7 @@ def get_or_create_response_cost_type(
return case_cost_type
-def get_all_response_case_cost_types(
- *, db_session, project_id: int
-) -> list[CaseCostType | None]:
+def get_all_response_case_cost_types(*, db_session, project_id: int) -> list[CaseCostType | None]:
"""Returns all response case cost types.
This function queries the database for all case cost types that are marked as the response cost type.
diff --git a/src/dispatch/conference/service.py b/src/dispatch/conference/service.py
index a65b8eb2591c..7e2384a33330 100644
--- a/src/dispatch/conference/service.py
+++ b/src/dispatch/conference/service.py
@@ -1,4 +1,3 @@
-
from .models import Conference, ConferenceCreate
diff --git a/src/dispatch/conversation/models.py b/src/dispatch/conversation/models.py
index e1e93e2b73e1..94754c35c73d 100644
--- a/src/dispatch/conversation/models.py
+++ b/src/dispatch/conversation/models.py
@@ -11,6 +11,7 @@
class Conversation(Base, ResourceMixin):
"""SQLAlchemy model for conversation resources."""
+
id = Column(Integer, primary_key=True)
channel_id = Column(String)
thread_id = Column(String)
@@ -22,22 +23,26 @@ class Conversation(Base, ResourceMixin):
# Pydantic models...
class ConversationBase(ResourceBase):
"""Base Pydantic model for conversation resources."""
+
channel_id: str | None = None
thread_id: str | None = None
class ConversationCreate(ConversationBase):
"""Pydantic model for creating a conversation resource."""
+
pass
class ConversationUpdate(ConversationBase):
"""Pydantic model for updating a conversation resource."""
+
pass
class ConversationRead(ConversationBase):
"""Pydantic model for reading a conversation resource."""
+
id: PrimaryKey
description: str | None = None
@@ -50,4 +55,5 @@ def set_description(cls, _):
class ConversationNested(ConversationBase):
"""Pydantic model for a nested conversation resource."""
+
pass
diff --git a/src/dispatch/data/alert/service.py b/src/dispatch/data/alert/service.py
index f144f187223a..c232d43224a7 100644
--- a/src/dispatch/data/alert/service.py
+++ b/src/dispatch/data/alert/service.py
@@ -1,4 +1,3 @@
-
from pydantic import ValidationError
diff --git a/src/dispatch/data/query/service.py b/src/dispatch/data/query/service.py
index 8fe6c9f554a6..d5655ddba126 100644
--- a/src/dispatch/data/query/service.py
+++ b/src/dispatch/data/query/service.py
@@ -27,14 +27,16 @@ def get_by_name_or_raise(*, db_session, query_in: QueryRead, project_id: int) ->
query = get_by_name(db_session=db_session, name=query_in.name, project_id=project_id)
if not query:
- raise ValidationError([
- {
- "loc": ("query",),
- "msg": f"Query not found: {query_in.name}",
- "type": "value_error",
- "input": query_in.name,
- }
- ])
+ raise ValidationError(
+ [
+ {
+ "loc": ("query",),
+ "msg": f"Query not found: {query_in.name}",
+ "type": "value_error",
+ "input": query_in.name,
+ }
+ ]
+ )
return query
diff --git a/src/dispatch/data/source/data_format/service.py b/src/dispatch/data/source/data_format/service.py
index 77a278c4fc3c..406030097c1c 100644
--- a/src/dispatch/data/source/data_format/service.py
+++ b/src/dispatch/data/source/data_format/service.py
@@ -38,14 +38,16 @@ def get_by_name_or_raise(
)
if not data_format:
- raise ValidationError([
- {
- "loc": ("dataFormat",),
- "msg": f"SourceDataFormat not found: {source_data_format_in.name}",
- "type": "value_error",
- "input": source_data_format_in.name,
- }
- ])
+ raise ValidationError(
+ [
+ {
+ "loc": ("dataFormat",),
+ "msg": f"SourceDataFormat not found: {source_data_format_in.name}",
+ "type": "value_error",
+ "input": source_data_format_in.name,
+ }
+ ]
+ )
return data_format
diff --git a/src/dispatch/data/source/environment/service.py b/src/dispatch/data/source/environment/service.py
index 8c131c5e58bb..b507098310cd 100644
--- a/src/dispatch/data/source/environment/service.py
+++ b/src/dispatch/data/source/environment/service.py
@@ -40,14 +40,16 @@ def get_by_name_or_raise(
)
if not source:
- raise ValidationError([
- {
- "loc": ("source",),
- "msg": f"Source environment not found: {source_environment_in.name}",
- "type": "value_error",
- "input": source_environment_in.name,
- }
- ])
+ raise ValidationError(
+ [
+ {
+ "loc": ("source",),
+ "msg": f"Source environment not found: {source_environment_in.name}",
+ "type": "value_error",
+ "input": source_environment_in.name,
+ }
+ ]
+ )
return source
diff --git a/src/dispatch/data/source/service.py b/src/dispatch/data/source/service.py
index eaf76f7c2ffe..bfad43af4b11 100644
--- a/src/dispatch/data/source/service.py
+++ b/src/dispatch/data/source/service.py
@@ -35,14 +35,16 @@ def get_by_name_or_raise(*, db_session, project_id, source_in: SourceRead) -> So
source = get_by_name(db_session=db_session, project_id=project_id, name=source_in.name)
if not source:
- raise ValidationError([
- {
- "loc": ("source",),
- "msg": f"Source not found: {source_in.name}",
- "type": "value_error",
- "input": source_in.name,
- }
- ])
+ raise ValidationError(
+ [
+ {
+ "loc": ("source",),
+ "msg": f"Source not found: {source_in.name}",
+ "type": "value_error",
+ "input": source_in.name,
+ }
+ ]
+ )
return source
diff --git a/src/dispatch/data/source/status/service.py b/src/dispatch/data/source/status/service.py
index 114271ea352f..e3f3ea2bc143 100644
--- a/src/dispatch/data/source/status/service.py
+++ b/src/dispatch/data/source/status/service.py
@@ -32,14 +32,16 @@ def get_by_name_or_raise(
status = get_by_name(db_session=db_session, project_id=project_id, name=source_status_in.name)
if not status:
- raise ValidationError([
- {
- "loc": ("status",),
- "msg": f"SourceStatus not found: {source_status_in.name}",
- "type": "value_error",
- "input": source_status_in.name,
- }
- ])
+ raise ValidationError(
+ [
+ {
+ "loc": ("status",),
+ "msg": f"SourceStatus not found: {source_status_in.name}",
+ "type": "value_error",
+ "input": source_status_in.name,
+ }
+ ]
+ )
return status
diff --git a/src/dispatch/data/source/transport/service.py b/src/dispatch/data/source/transport/service.py
index ba5106ccb9fb..2b6fe62c8e0a 100644
--- a/src/dispatch/data/source/transport/service.py
+++ b/src/dispatch/data/source/transport/service.py
@@ -38,14 +38,16 @@ def get_by_name_or_raise(
)
if not source:
- raise ValidationError([
- {
- "loc": ("source",),
- "msg": f"SourceTransport not found: {source_transport_in.name}",
- "type": "value_error",
- "input": source_transport_in.name,
- }
- ])
+ raise ValidationError(
+ [
+ {
+ "loc": ("source",),
+ "msg": f"SourceTransport not found: {source_transport_in.name}",
+ "type": "value_error",
+ "input": source_transport_in.name,
+ }
+ ]
+ )
return source
diff --git a/src/dispatch/data/source/type/service.py b/src/dispatch/data/source/type/service.py
index 34e9bfda7054..91843b3d9b10 100644
--- a/src/dispatch/data/source/type/service.py
+++ b/src/dispatch/data/source/type/service.py
@@ -32,14 +32,16 @@ def get_by_name_or_raise(
source = get_by_name(db_session=db_session, project_id=project_id, name=source_type_in.name)
if not source:
- raise ValidationError([
- {
- "loc": ("source",),
- "msg": f"SourceType not found: {source_type_in.name}",
- "type": "value_error",
- "input": source_type_in.name,
- }
- ])
+ raise ValidationError(
+ [
+ {
+ "loc": ("source",),
+ "msg": f"SourceType not found: {source_type_in.name}",
+ "type": "value_error",
+ "input": source_type_in.name,
+ }
+ ]
+ )
return source
diff --git a/src/dispatch/database/core.py b/src/dispatch/database/core.py
index a68726db472b..aeeb0b224d1d 100644
--- a/src/dispatch/database/core.py
+++ b/src/dispatch/database/core.py
@@ -94,6 +94,7 @@ def resolve_attr(obj, attr, default=None):
class Base(DeclarativeBase):
"""Base class for all SQLAlchemy models."""
+
__repr_attrs__ = []
__repr_max_length__ = 15
@@ -146,6 +147,8 @@ def __repr__(self):
id_str,
" " + self._repr_attrs_str if self._repr_attrs_str else "",
)
+
+
make_searchable(Base.metadata)
diff --git a/src/dispatch/database/manage.py b/src/dispatch/database/manage.py
index f56d4b5db279..903ca252e2fd 100644
--- a/src/dispatch/database/manage.py
+++ b/src/dispatch/database/manage.py
@@ -34,7 +34,7 @@ def version_schema(script_location: str):
alembic_command.stamp(alembic_cfg, "head")
-def get_core_tables() -> list[Table]:
+def get_core_tables() -> list[Table]:
"""Fetches tables that belong to the 'dispatch_core' schema."""
core_tables: list[Table] = []
for _, table in Base.metadata.tables.items():
diff --git a/src/dispatch/database/revisions/core/README b/src/dispatch/database/revisions/core/README
index 98e4f9c44eff..2500aa1bcf72 100644
--- a/src/dispatch/database/revisions/core/README
+++ b/src/dispatch/database/revisions/core/README
@@ -1 +1 @@
-Generic single-database configuration.
\ No newline at end of file
+Generic single-database configuration.
diff --git a/src/dispatch/database/revisions/core/env.py b/src/dispatch/database/revisions/core/env.py
index 0ae70a6c01f2..a7b867588e21 100644
--- a/src/dispatch/database/revisions/core/env.py
+++ b/src/dispatch/database/revisions/core/env.py
@@ -34,6 +34,7 @@ def run_migrations_online():
and associate a connection with the context.
"""
+
def process_revision_directives(context, revision, directives):
script = directives[0]
if script.upgrade_ops.is_empty():
diff --git a/src/dispatch/database/revisions/tenant/README b/src/dispatch/database/revisions/tenant/README
index 98e4f9c44eff..2500aa1bcf72 100644
--- a/src/dispatch/database/revisions/tenant/README
+++ b/src/dispatch/database/revisions/tenant/README
@@ -1 +1 @@
-Generic single-database configuration.
\ No newline at end of file
+Generic single-database configuration.
diff --git a/src/dispatch/database/service.py b/src/dispatch/database/service.py
index 96de7519a00d..0be92b6c80c0 100644
--- a/src/dispatch/database/service.py
+++ b/src/dispatch/database/service.py
@@ -273,8 +273,8 @@ def get_query_models(query):
# Try to get the statement from the query
stmt = query.statement
- # Extract entities from the statement's froms
- for from_obj in stmt.froms:
+ # Extract entities from the statement's forms
+ for from_obj in stmt.forms:
if hasattr(from_obj, "entity"):
# For select statements with an entity
if from_obj.entity not in models:
diff --git a/src/dispatch/definition/service.py b/src/dispatch/definition/service.py
index 958d10ac3dbb..f603d85e1bdf 100644
--- a/src/dispatch/definition/service.py
+++ b/src/dispatch/definition/service.py
@@ -1,4 +1,3 @@
-
from dispatch.project import service as project_service
from dispatch.term import service as term_service
diff --git a/src/dispatch/definition/views.py b/src/dispatch/definition/views.py
index 9831d5fffe41..ceb50cb7ae1c 100644
--- a/src/dispatch/definition/views.py
+++ b/src/dispatch/definition/views.py
@@ -39,12 +39,14 @@ def create_definition(db_session: DbSession, definition_in: DefinitionCreate):
"""Create a new definition."""
definition = get_by_text(db_session=db_session, text=definition_in.text)
if definition:
- raise ValidationError([
- {
- "msg": "A description with this text already exists.",
- "loc": "text",
- }
- ])
+ raise ValidationError(
+ [
+ {
+ "msg": "A description with this text already exists.",
+ "loc": "text",
+ }
+ ]
+ )
return create(db_session=db_session, definition_in=definition_in)
diff --git a/src/dispatch/document/service.py b/src/dispatch/document/service.py
index 21deb5866fe5..90f4cc8b865b 100644
--- a/src/dispatch/document/service.py
+++ b/src/dispatch/document/service.py
@@ -89,12 +89,14 @@ def create(*, db_session, document_in: DocumentCreate) -> Document:
.one_or_none()
)
if faq_doc:
- raise ValidationError([
- {
- "msg": "FAQ document already defined for this project.",
- "loc": "document",
- }
- ])
+ raise ValidationError(
+ [
+ {
+ "msg": "FAQ document already defined for this project.",
+ "loc": "document",
+ }
+ ]
+ )
if document_in.resource_type == DocumentResourceTemplateTypes.forms:
forms_doc = (
@@ -104,12 +106,14 @@ def create(*, db_session, document_in: DocumentCreate) -> Document:
.one_or_none()
)
if forms_doc:
- raise ValidationError([
- {
- "msg": "Forms export template document already defined for this project.",
- "loc": "document",
- }
- ])
+ raise ValidationError(
+ [
+ {
+ "msg": "Forms export template document already defined for this project.",
+ "loc": "document",
+ }
+ ]
+ )
filters = [
search_filter_service.get(db_session=db_session, search_filter_id=f.id)
diff --git a/src/dispatch/entity_type/views.py b/src/dispatch/entity_type/views.py
index 162600fbf88c..ce951096a050 100644
--- a/src/dispatch/entity_type/views.py
+++ b/src/dispatch/entity_type/views.py
@@ -1,4 +1,3 @@
-
from fastapi import APIRouter, HTTPException, status
from pydantic import ValidationError
from sqlalchemy.exc import IntegrityError
diff --git a/src/dispatch/event/service.py b/src/dispatch/event/service.py
index aa1d7852843a..52032e5ce73a 100644
--- a/src/dispatch/event/service.py
+++ b/src/dispatch/event/service.py
@@ -492,7 +492,7 @@ def export_timeline(
}
)
- # Formating for date rows
+ # Formatting for date rows
if text == "\t":
insert_data_request.append(
{
@@ -518,7 +518,7 @@ def export_timeline(
}
)
- # Formating for time column
+ # Formatting for time column
if row_idx % num_columns == 0:
insert_data_request.append(
{
diff --git a/src/dispatch/feedback/incident/service.py b/src/dispatch/feedback/incident/service.py
index 8d8d45d8c8c9..6e077b46ef11 100644
--- a/src/dispatch/feedback/incident/service.py
+++ b/src/dispatch/feedback/incident/service.py
@@ -68,9 +68,9 @@ def create(*, db_session, feedback_in: FeedbackCreate) -> Feedback:
participant = None
if feedback_in.participant:
from dispatch.participant.service import get as get_participant
+
participant = get_participant(
- db_session=db_session,
- participant_id=feedback_in.participant.id
+ db_session=db_session, participant_id=feedback_in.participant.id
)
# Create feedback with the actual ORM objects, not the Pydantic models
@@ -80,7 +80,7 @@ def create(*, db_session, feedback_in: FeedbackCreate) -> Feedback:
incident=incident,
case=case,
project=project,
- participant=participant
+ participant=participant,
)
db_session.add(feedback)
db_session.commit()
diff --git a/src/dispatch/feedback/service/service.py b/src/dispatch/feedback/service/service.py
index 75b0c7f2794d..18ef42fd2b57 100644
--- a/src/dispatch/feedback/service/service.py
+++ b/src/dispatch/feedback/service/service.py
@@ -1,4 +1,3 @@
-
from sqlalchemy.orm import Session
from .models import ServiceFeedback, ServiceFeedbackCreate, ServiceFeedbackUpdate
diff --git a/src/dispatch/group/models.py b/src/dispatch/group/models.py
index 58e962ec45cf..2327df1b4e0a 100644
--- a/src/dispatch/group/models.py
+++ b/src/dispatch/group/models.py
@@ -1,4 +1,5 @@
"""Models for group resources in the Dispatch application."""
+
from pydantic import field_validator, EmailStr
from sqlalchemy import Column, Integer, String, ForeignKey
@@ -11,6 +12,7 @@
class Group(Base, ResourceMixin):
"""SQLAlchemy model for group resources."""
+
id = Column(Integer, primary_key=True)
name = Column(String)
email = Column(String)
@@ -21,22 +23,26 @@ class Group(Base, ResourceMixin):
# Pydantic models...
class GroupBase(ResourceBase):
"""Base Pydantic model for group resources."""
+
name: NameStr
email: EmailStr
class GroupCreate(GroupBase):
"""Pydantic model for creating a group resource."""
+
pass
class GroupUpdate(GroupBase):
"""Pydantic model for updating a group resource."""
+
id: PrimaryKey | None = None
class GroupRead(GroupBase):
"""Pydantic model for reading a group resource."""
+
id: PrimaryKey
description: str | None = None
diff --git a/src/dispatch/group/service.py b/src/dispatch/group/service.py
index be889044bc3f..e55fc5d36fdc 100644
--- a/src/dispatch/group/service.py
+++ b/src/dispatch/group/service.py
@@ -1,4 +1,3 @@
-
from .models import Group, GroupCreate, GroupUpdate
diff --git a/src/dispatch/incident/messaging.py b/src/dispatch/incident/messaging.py
index 7fa74e3873ee..5c8d4b1edc79 100644
--- a/src/dispatch/incident/messaging.py
+++ b/src/dispatch/incident/messaging.py
@@ -822,7 +822,7 @@ def send_incident_participant_has_role_ephemeral_message(
)
if not plugin:
log.warning(
- "Unabled to send incident participant has role message, no conversation plugin enabled."
+ "Unable to send incident participant has role message, no conversation plugin enabled."
)
return
@@ -862,7 +862,7 @@ def send_incident_participant_role_not_assigned_ephemeral_message(
)
if not plugin:
log.warning(
- "Unabled to send incident participant role not assigned message, no conversation plugin enabled." # noqa
+ "Unable to send incident participant role not assigned message, no conversation plugin enabled." # noqa
)
return
diff --git a/src/dispatch/incident/service.py b/src/dispatch/incident/service.py
index 34c3c20189c3..29da216a9185 100644
--- a/src/dispatch/incident/service.py
+++ b/src/dispatch/incident/service.py
@@ -95,12 +95,14 @@ def get_by_name_or_raise(
incident = get_by_name(db_session=db_session, project_id=project_id, name=incident_in.name)
if not incident:
- raise ValidationError([
- {
- "msg": "Incident not found.",
- "loc": "name",
- }
- ])
+ raise ValidationError(
+ [
+ {
+ "msg": "Incident not found.",
+ "loc": "name",
+ }
+ ]
+ )
return incident
diff --git a/src/dispatch/incident/severity/models.py b/src/dispatch/incident/severity/models.py
index dc3ba1efe9d1..d5215317a575 100644
--- a/src/dispatch/incident/severity/models.py
+++ b/src/dispatch/incident/severity/models.py
@@ -9,8 +9,10 @@
from dispatch.models import DispatchBase, NameStr, ProjectMixin, PrimaryKey, Pagination
from dispatch.project.models import ProjectRead
+
class IncidentSeverity(Base, ProjectMixin):
"""SQLAlchemy model for incident severity resources."""
+
__table_args__ = (UniqueConstraint("name", "project_id"),)
id = Column(Integer, primary_key=True)
name = Column(String)
@@ -39,6 +41,7 @@ class IncidentSeverity(Base, ProjectMixin):
# Pydantic models
class IncidentSeverityBase(DispatchBase):
"""Base Pydantic model for incident severity resources."""
+
color: str | None = None
default: bool | None = None
description: str | None = None
@@ -51,21 +54,25 @@ class IncidentSeverityBase(DispatchBase):
class IncidentSeverityCreate(IncidentSeverityBase):
"""Pydantic model for creating an incident severity resource."""
+
pass
class IncidentSeverityUpdate(IncidentSeverityBase):
"""Pydantic model for updating an incident severity resource."""
+
pass
class IncidentSeverityRead(IncidentSeverityBase):
"""Pydantic model for reading an incident severity resource."""
+
id: PrimaryKey
class IncidentSeverityReadMinimal(DispatchBase):
"""Pydantic model for reading a minimal incident severity resource."""
+
id: PrimaryKey
color: str | None = None
default: bool | None = None
@@ -77,4 +84,5 @@ class IncidentSeverityReadMinimal(DispatchBase):
class IncidentSeverityPagination(Pagination):
"""Pydantic model for paginated incident severity results."""
+
items: list[IncidentSeverityRead] = []
diff --git a/src/dispatch/incident/type/models.py b/src/dispatch/incident/type/models.py
index d147838203c5..26f3f0b1c9e7 100644
--- a/src/dispatch/incident/type/models.py
+++ b/src/dispatch/incident/type/models.py
@@ -19,8 +19,10 @@
from dispatch.project.models import ProjectRead
from dispatch.service.models import ServiceRead
+
class IncidentType(ProjectMixin, Base):
"""SQLAlchemy model for incident type resources."""
+
__table_args__ = (UniqueConstraint("name", "project_id"),)
id = Column(Integer, primary_key=True)
name = Column(String)
@@ -99,6 +101,7 @@ def get_task_meta(self, slug):
class Document(DispatchBase):
"""Pydantic model for a document related to an incident type."""
+
id: PrimaryKey
name: NameStr
resource_type: str | None = None
@@ -110,6 +113,7 @@ class Document(DispatchBase):
# Pydantic models...
class IncidentTypeBase(DispatchBase):
"""Base Pydantic model for incident type resources."""
+
name: NameStr
visibility: str | None = None
description: str | None = None
@@ -138,21 +142,25 @@ def replace_none_with_empty_list(cls, value):
class IncidentTypeCreate(IncidentTypeBase):
"""Pydantic model for creating an incident type resource."""
+
pass
class IncidentTypeUpdate(IncidentTypeBase):
"""Pydantic model for updating an incident type resource."""
+
id: PrimaryKey | None = None
class IncidentTypeRead(IncidentTypeBase):
"""Pydantic model for reading an incident type resource."""
+
id: PrimaryKey
class IncidentTypeReadMinimal(DispatchBase):
"""Pydantic model for reading a minimal incident type resource."""
+
id: PrimaryKey
name: NameStr
visibility: str | None = None
@@ -164,4 +172,5 @@ class IncidentTypeReadMinimal(DispatchBase):
class IncidentTypePagination(Pagination):
"""Pydantic model for paginated incident type results."""
+
items: list[IncidentTypeRead] = []
diff --git a/src/dispatch/incident_cost_type/models.py b/src/dispatch/incident_cost_type/models.py
index 29228e47ecd2..66408dd4fd5b 100644
--- a/src/dispatch/incident_cost_type/models.py
+++ b/src/dispatch/incident_cost_type/models.py
@@ -20,6 +20,7 @@
# SQLAlchemy Model
class IncidentCostType(Base, TimeStampMixin, ProjectMixin):
"""SQLAlchemy model for incident cost type resources."""
+
# columns
id = Column(Integer, primary_key=True)
name = Column(String)
@@ -41,6 +42,7 @@ class IncidentCostType(Base, TimeStampMixin, ProjectMixin):
# Pydantic Models
class IncidentCostTypeBase(DispatchBase):
"""Base Pydantic model for incident cost type resources."""
+
name: NameStr
description: str | None = None
category: str | None = None
@@ -51,20 +53,24 @@ class IncidentCostTypeBase(DispatchBase):
class IncidentCostTypeCreate(IncidentCostTypeBase):
"""Pydantic model for creating an incident cost type."""
+
project: ProjectRead
class IncidentCostTypeUpdate(IncidentCostTypeBase):
"""Pydantic model for updating an incident cost type."""
+
id: PrimaryKey | None = None
class IncidentCostTypeRead(IncidentCostTypeBase):
"""Pydantic model for reading an incident cost type."""
+
id: PrimaryKey
created_at: datetime
class IncidentCostTypePagination(Pagination):
"""Pydantic model for paginated incident cost type results."""
+
items: list[IncidentCostTypeRead] = []
diff --git a/src/dispatch/incident_cost_type/service.py b/src/dispatch/incident_cost_type/service.py
index bc6bdeb0bb12..6d6eb7483da4 100644
--- a/src/dispatch/incident_cost_type/service.py
+++ b/src/dispatch/incident_cost_type/service.py
@@ -50,9 +50,7 @@ def create(*, db_session, incident_cost_type_in: IncidentCostTypeCreate) -> Inci
project = project_service.get_by_name_or_raise(
db_session=db_session, project_in=incident_cost_type_in.project
)
- incident_cost_type = IncidentCostType(
- **incident_cost_type_in.dict(exclude={"project"})
- )
+ incident_cost_type = IncidentCostType(**incident_cost_type_in.dict(exclude={"project"}))
incident_cost_type.project = project # type: ignore[attr-defined]
db_session.add(incident_cost_type)
db_session.commit()
diff --git a/src/dispatch/incident_role/service.py b/src/dispatch/incident_role/service.py
index 53e21619cf5f..4b5ba4de3677 100644
--- a/src/dispatch/incident_role/service.py
+++ b/src/dispatch/incident_role/service.py
@@ -84,7 +84,7 @@ def create_or_update(
"msg": "Incident role not found.",
"input": role_policy_in.name,
}
- ]
+ ],
)
else:
diff --git a/src/dispatch/individual/service.py b/src/dispatch/individual/service.py
index 0a664db10a8a..5edc691c151b 100644
--- a/src/dispatch/individual/service.py
+++ b/src/dispatch/individual/service.py
@@ -53,14 +53,16 @@ def get_by_email_and_project_id_or_raise(
)
if not individual_contact:
- raise ValidationError([
- {
- "loc": ("individual",),
- "msg": "Individual not found.",
- "type": "value_error",
- "input": individual_contact_in.email,
- }
- ])
+ raise ValidationError(
+ [
+ {
+ "loc": ("individual",),
+ "msg": "Individual not found.",
+ "type": "value_error",
+ "input": individual_contact_in.email,
+ }
+ ]
+ )
return individual_contact
diff --git a/src/dispatch/individual/views.py b/src/dispatch/individual/views.py
index 5d362955a7e0..3214fd2bf692 100644
--- a/src/dispatch/individual/views.py
+++ b/src/dispatch/individual/views.py
@@ -36,7 +36,7 @@ def get_individual(db_session: DbSession, individual_contact_id: PrimaryKey):
"msg": "Individual not found.",
"input": individual_contact_id,
}
- ]
+ ],
)
return individual
@@ -56,12 +56,14 @@ def create_individual(db_session: DbSession, individual_contact_in: IndividualCo
project_id=individual_contact_in.project.id,
)
if individual:
- raise ValidationError([
- {
- "msg": "An individual with this email already exists.",
- "loc": "email",
- }
- ])
+ raise ValidationError(
+ [
+ {
+ "msg": "An individual with this email already exists.",
+ "loc": "email",
+ }
+ ]
+ )
return create(db_session=db_session, individual_contact_in=individual_contact_in)
@@ -88,7 +90,7 @@ def update_individual(
"msg": "Individual not found.",
"input": individual_contact_id,
}
- ]
+ ],
)
return update(
db_session=db_session,
@@ -116,6 +118,6 @@ def delete_individual(db_session: DbSession, individual_contact_id: PrimaryKey):
"msg": "Individual not found.",
"input": individual_contact_id,
}
- ]
+ ],
)
delete(db_session=db_session, individual_contact_id=individual_contact_id)
diff --git a/src/dispatch/messaging/strings.py b/src/dispatch/messaging/strings.py
index a791e7e2c3c4..5dcb194393f7 100644
--- a/src/dispatch/messaging/strings.py
+++ b/src/dispatch/messaging/strings.py
@@ -78,36 +78,24 @@ class MessageType(DispatchEnum):
).strip()
INCIDENT_FEEDBACK_DAILY_REPORT_DESCRIPTION = """
-This is a daily report of feedback about incidents handled by you.""".replace(
- "\n", " "
-).strip()
+This is a daily report of feedback about incidents handled by you.""".replace("\n", " ").strip()
CASE_FEEDBACK_DAILY_REPORT_DESCRIPTION = """
-This is a daily report of feedback about cases handled by you.""".replace(
- "\n", " "
-).strip()
+This is a daily report of feedback about cases handled by you.""".replace("\n", " ").strip()
INCIDENT_WEEKLY_REPORT_TITLE = """
-Incidents Weekly Report""".replace(
- "\n", " "
-).strip()
+Incidents Weekly Report""".replace("\n", " ").strip()
INCIDENT_WEEKLY_REPORT_DESCRIPTION = """
This is an AI-generated weekly summary of incidents that have been marked as closed in the last week.
NOTE: These summaries may contain errors or inaccuracies.
-Please verify the information before relying on it.""".replace(
- "\n", " "
-).strip()
+Please verify the information before relying on it.""".replace("\n", " ").strip()
INCIDENT_WEEKLY_REPORT_NO_INCIDENTS_DESCRIPTION = """
-No open visibility incidents have been closed in the last week.""".replace(
- "\n", " "
-).strip()
+No open visibility incidents have been closed in the last week.""".replace("\n", " ").strip()
INCIDENT_DAILY_REPORT_TITLE = """
-Incidents Daily Report""".replace(
- "\n", " "
-).strip()
+Incidents Daily Report""".replace("\n", " ").strip()
INCIDENT_DAILY_REPORT_DESCRIPTION = """
This is a daily report of incidents that are currently active and incidents that have been marked as stable or closed in the last 24 hours.""".replace(
@@ -128,9 +116,7 @@ class MessageType(DispatchEnum):
INCIDENT_COMMANDER_DESCRIPTION = """
The Incident Commander (IC) is responsible for
knowing the full context of the incident.
-Contact them about any questions or concerns.""".replace(
- "\n", " "
-).strip()
+Contact them about any questions or concerns.""".replace("\n", " ").strip()
INCIDENT_COMMANDER_READDED_DESCRIPTION = """
{{ commander_fullname }} (Incident Commander) has been re-added to the conversation.
@@ -155,75 +141,53 @@ class MessageType(DispatchEnum):
INCIDENT_CONVERSATION_DESCRIPTION = """
Private conversation for real-time discussion. All incident participants get added to it.
-""".replace(
- "\n", " "
-).strip()
+""".replace("\n", " ").strip()
CASE_CONVERSATION_REFERENCE_DOCUMENT_DESCRIPTION = """
Document containing the list of slash commands available to the Assignee
-and participants in the case conversation.""".replace(
- "\n", " "
-).strip()
+and participants in the case conversation.""".replace("\n", " ").strip()
INCIDENT_CONVERSATION_REFERENCE_DOCUMENT_DESCRIPTION = """
Document containing the list of slash commands available to the Incident Commander (IC)
-and participants in the incident conversation.""".replace(
- "\n", " "
-).strip()
+and participants in the incident conversation.""".replace("\n", " ").strip()
CASE_CONFERENCE_DESCRIPTION = """
Video conference and phone bridge to be used throughout the case. Password: {{conference_challenge if conference_challenge else 'N/A'}}
-""".replace(
- "\n", ""
-).strip()
+""".replace("\n", "").strip()
INCIDENT_CONFERENCE_DESCRIPTION = """
Video conference and phone bridge to be used throughout the incident. Password: {{conference_challenge if conference_challenge else 'N/A'}}
-""".replace(
- "\n", ""
-).strip()
+""".replace("\n", "").strip()
STORAGE_DESCRIPTION = """
Common storage for all artifacts and
documents. Add logs, screen captures, or any other data collected during the
-investigation to this folder. It is shared with all participants.""".replace(
- "\n", " "
-).strip()
+investigation to this folder. It is shared with all participants.""".replace("\n", " ").strip()
INCIDENT_INVESTIGATION_DOCUMENT_DESCRIPTION = """
This is a document for all incident facts and context. All
incident participants are expected to contribute to this document.
-It is shared with all incident participants.""".replace(
- "\n", " "
-).strip()
+It is shared with all incident participants.""".replace("\n", " ").strip()
CASE_INVESTIGATION_DOCUMENT_DESCRIPTION = """
This is a document for all investigation facts and context. All
case participants are expected to contribute to this document.
-It is shared with all participants.""".replace(
- "\n", " "
-).strip()
+It is shared with all participants.""".replace("\n", " ").strip()
INCIDENT_INVESTIGATION_SHEET_DESCRIPTION = """
This is a sheet for tracking impacted assets. All
incident participants are expected to contribute to this sheet.
-It is shared with all incident participants.""".replace(
- "\n", " "
-).strip()
+It is shared with all incident participants.""".replace("\n", " ").strip()
CASE_FAQ_DOCUMENT_DESCRIPTION = """
First time responding to a case? This
document answers common questions encountered when
-helping us respond to a case.""".replace(
- "\n", " "
-).strip()
+helping us respond to a case.""".replace("\n", " ").strip()
INCIDENT_FAQ_DOCUMENT_DESCRIPTION = """
First time responding to an incident? This
document answers common questions encountered when
-helping us respond to an incident.""".replace(
- "\n", " "
-).strip()
+helping us respond to an incident.""".replace("\n", " ").strip()
INCIDENT_REVIEW_DOCUMENT_DESCRIPTION = """
This document will capture all lessons learned, questions, and action items raised during the incident.""".replace(
@@ -247,15 +211,11 @@ class MessageType(DispatchEnum):
INCIDENT_RESOLUTION_DEFAULT = """
Description of the actions taken to resolve the incident.
-""".replace(
- "\n", " "
-).strip()
+""".replace("\n", " ").strip()
CASE_RESOLUTION_DEFAULT = """
Description of the actions taken to resolve the case.
-""".replace(
- "\n", " "
-).strip()
+""".replace("\n", " ").strip()
INCIDENT_COMPLETED_FORM_DESCRIPTION = """
A new {{form_type}} form related to incident {{name}} has been
@@ -263,34 +223,24 @@ class MessageType(DispatchEnum):
aspects related to potential legal implications. You can review the
detailed report by clicking on the link below. Please note, the information
contained in this report is confidential.
-""".replace(
- "\n", " "
-).strip()
+""".replace("\n", " ").strip()
CASE_PARTICIPANT_WELCOME_DESCRIPTION = """
You\'ve been added to this case, because we think you may
be able to help resolve it. Please review the case details below and
-reach out to the assignee if you have any questions.""".replace(
- "\n", " "
-).strip()
+reach out to the assignee if you have any questions.""".replace("\n", " ").strip()
INCIDENT_PARTICIPANT_WELCOME_DESCRIPTION = """
You\'ve been added to this incident, because we think you may
be able to help resolve it. Please review the incident details below and
-reach out to the incident commander if you have any questions.""".replace(
- "\n", " "
-).strip()
+reach out to the incident commander if you have any questions.""".replace("\n", " ").strip()
INCIDENT_PARTICIPANT_SUGGESTED_READING_DESCRIPTION = """
Dispatch thinks the following documents might be
-relevant to this incident.""".replace(
- "\n", " "
-).strip()
+relevant to this incident.""".replace("\n", " ").strip()
NOTIFICATION_PURPOSES_FYI = """
-This message is for notification purposes only.""".replace(
- "\n", " "
-).strip()
+This message is for notification purposes only.""".replace("\n", " ").strip()
INCIDENT_TACTICAL_REPORT_DESCRIPTION = """
The following conditions, actions, and needs summarize the current status of the incident.""".replace(
@@ -319,9 +269,7 @@ class MessageType(DispatchEnum):
).strip()
CASE_TRIAGE_REMINDER_DESCRIPTION = """The status of this case hasn't been updated recently.
-Please ensure you triage the case based on its priority.""".replace(
- "\n", " "
-).strip()
+Please ensure you triage the case based on its priority.""".replace("\n", " ").strip()
CASE_CLOSE_REMINDER_DESCRIPTION = """The status of this case hasn't been updated recently.
You can use the case 'Resolve' button if it has been resolved and can be closed.""".replace(
@@ -346,9 +294,7 @@ class MessageType(DispatchEnum):
INCIDENT_OPEN_TASKS_DESCRIPTION = """
Please resolve or transfer ownership of all the open incident tasks assigned to you in the incident documents or using the <{{dispatch_ui_url}}|Dispatch Web UI>,
then wait about 30 seconds for Dispatch to update the tasks before leaving the incident conversation.
-""".replace(
- "\n", " "
-).strip()
+""".replace("\n", " ").strip()
INCIDENT_TASK_ADD_TO_INCIDENT_DESCRIPTION = """
You have been added to this incident because you were assigned a task related to it. View all tasks for this incident using the <{{dispatch_ui_url}}|Dispatch Web UI>
@@ -874,9 +820,7 @@ class MessageType(DispatchEnum):
CASE_ASSIGNEE_DESCRIPTION = """
The Case Assignee is responsible for
knowing the full context of the case.
-Contact them about any questions or concerns.""".replace(
- "\n", " "
-).strip()
+Contact them about any questions or concerns.""".replace("\n", " ").strip()
CASE_REPORTER_DESCRIPTION = """
The person who reported the case. Contact them if the report details need clarification.""".replace(
diff --git a/src/dispatch/models.py b/src/dispatch/models.py
index 19822c9e7cc9..9d1cc684b739 100644
--- a/src/dispatch/models.py
+++ b/src/dispatch/models.py
@@ -85,7 +85,10 @@ class EvergreenMixin(object):
def overdue(self):
"""Returns True if the evergreen reminder is overdue."""
now = datetime.now(timezone.utc)
- if self.evergreen_last_reminder_at is not None and self.evergreen_reminder_interval is not None:
+ if (
+ self.evergreen_last_reminder_at is not None
+ and self.evergreen_reminder_interval is not None
+ ):
next_reminder = self.evergreen_last_reminder_at + timedelta(
days=self.evergreen_reminder_interval
)
@@ -112,6 +115,7 @@ class FeedbackMixin(object):
# Pydantic models...
class DispatchBase(BaseModel):
"""Base Pydantic model with shared config for Dispatch models."""
+
model_config: ClassVar[ConfigDict] = ConfigDict(
from_attributes=True,
validate_assignment=True,
@@ -127,6 +131,7 @@ class DispatchBase(BaseModel):
class Pagination(DispatchBase):
"""Pydantic model for paginated results."""
+
itemsPerPage: int
page: int
total: int
@@ -134,11 +139,13 @@ class Pagination(DispatchBase):
class PrimaryKeyModel(BaseModel):
"""Pydantic model for a primary key field."""
+
id: PrimaryKey
class EvergreenBase(DispatchBase):
"""Base Pydantic model for evergreen resources."""
+
evergreen: bool | None = False
evergreen_owner: EmailStr | None = None
evergreen_reminder_interval: int | None = 90
@@ -147,6 +154,7 @@ class EvergreenBase(DispatchBase):
class ResourceBase(DispatchBase):
"""Base Pydantic model for resource-related fields."""
+
resource_type: str | None = None
resource_id: str | None = None
weblink: str | None = None
@@ -154,6 +162,7 @@ class ResourceBase(DispatchBase):
class ContactBase(DispatchBase):
"""Base Pydantic model for contact-related fields."""
+
email: EmailStr
name: str | None = None
is_active: bool | None = True
diff --git a/src/dispatch/monitor/service.py b/src/dispatch/monitor/service.py
index 2c19c453b9d9..3e941aa0e502 100644
--- a/src/dispatch/monitor/service.py
+++ b/src/dispatch/monitor/service.py
@@ -1,4 +1,3 @@
-
from sqlalchemy.sql.expression import true
from dispatch.incident import service as incident_service
from dispatch.plugin import service as plugin_service
diff --git a/src/dispatch/organization/models.py b/src/dispatch/organization/models.py
index 8455f9daf117..41b543f1ef0c 100644
--- a/src/dispatch/organization/models.py
+++ b/src/dispatch/organization/models.py
@@ -12,6 +12,7 @@
class Organization(Base):
"""SQLAlchemy model for organization resources."""
+
__table_args__ = {"schema": "dispatch_core"}
id = Column(Integer, primary_key=True)
@@ -39,6 +40,7 @@ def generate_slug(target, value, oldvalue, initiator):
class OrganizationBase(DispatchBase):
"""Base Pydantic model for organization resources."""
+
id: PrimaryKey | None = None
name: NameStr
description: str | None = None
@@ -50,11 +52,13 @@ class OrganizationBase(DispatchBase):
class OrganizationCreate(OrganizationBase):
"""Pydantic model for creating an organization resource."""
+
pass
class OrganizationUpdate(DispatchBase):
"""Pydantic model for updating an organization resource."""
+
id: PrimaryKey | None = None
description: str | None = None
default: bool | None = False
@@ -65,10 +69,12 @@ class OrganizationUpdate(DispatchBase):
class OrganizationRead(OrganizationBase):
"""Pydantic model for reading an organization resource."""
+
id: PrimaryKey | None = None
slug: OrganizationSlug | None = None
class OrganizationPagination(Pagination):
"""Pydantic model for paginated organization results."""
+
items: list[OrganizationRead] = []
diff --git a/src/dispatch/participant/service.py b/src/dispatch/participant/service.py
index ea873a4bd253..355523342fea 100644
--- a/src/dispatch/participant/service.py
+++ b/src/dispatch/participant/service.py
@@ -60,9 +60,7 @@ def get_by_incident_id_and_role(
)
-def get_by_case_id_and_role(
- *, db_session: Session, case_id: int, role: str
-) -> Participant | None:
+def get_by_case_id_and_role(*, db_session: Session, case_id: int, role: str) -> Participant | None:
"""Get a participant by case id and role name."""
return (
db_session.query(Participant)
diff --git a/src/dispatch/plugin/service.py b/src/dispatch/plugin/service.py
index c87e2600801d..fc5c1064bd5a 100644
--- a/src/dispatch/plugin/service.py
+++ b/src/dispatch/plugin/service.py
@@ -152,12 +152,14 @@ def update_instance(
db_session=db_session, service_type=plugin_instance.plugin.slug, is_active=True
)
if oncall_services:
- raise ValidationError([
- {
- "msg": "Cannot disable plugin instance: {plugin_instance.plugin.title}. One or more oncall services depend on it. ",
- "loc": "plugin_instance",
- }
- ])
+ raise ValidationError(
+ [
+ {
+ "msg": "Cannot disable plugin instance: {plugin_instance.plugin.title}. One or more oncall services depend on it. ",
+ "loc": "plugin_instance",
+ }
+ ]
+ )
for field in plugin_instance_data:
if field in update_data:
@@ -185,9 +187,7 @@ def get_plugin_event_by_slug(*, db_session: Session, slug: str) -> PluginEvent |
return db_session.query(PluginEvent).filter(PluginEvent.slug == slug).one_or_none()
-def get_all_events_for_plugin(
- *, db_session: Session, plugin_id: int
-) -> list[PluginEvent | None]:
+def get_all_events_for_plugin(*, db_session: Session, plugin_id: int) -> list[PluginEvent | None]:
"""Returns all plugin events for a given plugin."""
return db_session.query(PluginEvent).filter(PluginEvent.plugin_id == plugin_id).all()
diff --git a/src/dispatch/plugins/dispatch_core/plugin.py b/src/dispatch/plugins/dispatch_core/plugin.py
index 43acb74e1e7d..b9f0df525ad2 100644
--- a/src/dispatch/plugins/dispatch_core/plugin.py
+++ b/src/dispatch/plugins/dispatch_core/plugin.py
@@ -181,7 +181,11 @@ class AwsAlbAuthProviderPlugin(AuthenticationProviderPlugin):
author_url = "https://manypets.com/"
configuration_schema = None
- @cached(cache=TTLCache(maxsize=1024, ttl=DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_PUBLIC_KEY_CACHE_SECONDS))
+ @cached(
+ cache=TTLCache(
+ maxsize=1024, ttl=DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_PUBLIC_KEY_CACHE_SECONDS
+ )
+ )
def get_public_key(self, kid: str, region: str):
log.debug("Cache miss. Requesting key from AWS endpoint.")
url = f"https://public-keys.auth.elb.{region}.amazonaws.com/{kid}"
@@ -193,20 +197,18 @@ def get_current_user(self, request: Request, **kwargs):
status_code=HTTP_401_UNAUTHORIZED, detail=[{"msg": "Could not validate credentials"}]
)
- encoded_jwt: str = request.headers.get('x-amzn-oidc-data')
+ encoded_jwt: str = request.headers.get("x-amzn-oidc-data")
if not encoded_jwt:
- log.error(
- "Unable to authenticate. Header x-amzn-oidc-data not found."
- )
+ log.error("Unable to authenticate. Header x-amzn-oidc-data not found.")
raise credentials_exception
log.debug(f"Header x-amzn-oidc-data header received: {encoded_jwt}")
# Validate the signer
- jwt_headers = encoded_jwt.split('.')[0]
+ jwt_headers = encoded_jwt.split(".")[0]
decoded_jwt_headers = base64.b64decode(jwt_headers)
decoded_json = json.loads(decoded_jwt_headers)
- received_alb_arn = decoded_json['signer']
+ received_alb_arn = decoded_json["signer"]
if received_alb_arn != DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_ARN:
log.error(
@@ -215,10 +217,10 @@ def get_current_user(self, request: Request, **kwargs):
raise credentials_exception
# Get the key id from JWT headers (the kid field)
- kid = decoded_json['kid']
+ kid = decoded_json["kid"]
# Get the region from the ARN
- region = DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_ARN.split(':')[3]
+ region = DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_ARN.split(":")[3]
# Get the public key from regional endpoint
log.debug(f"Getting public key for kid {kid} in region {region}.")
@@ -226,7 +228,7 @@ def get_current_user(self, request: Request, **kwargs):
# Get the payload
log.debug(f"Decoding {encoded_jwt} with public key {pub_key}.")
- payload = jwt.decode(encoded_jwt, pub_key, algorithms=['ES256'])
+ payload = jwt.decode(encoded_jwt, pub_key, algorithms=["ES256"])
return payload[DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_EMAIL_CLAIM]
diff --git a/src/dispatch/plugins/dispatch_google/drive/task.py b/src/dispatch/plugins/dispatch_google/drive/task.py
index cbb4401da71c..36cd93f11ef7 100644
--- a/src/dispatch/plugins/dispatch_google/drive/task.py
+++ b/src/dispatch/plugins/dispatch_google/drive/task.py
@@ -49,7 +49,10 @@ def find_urls(text: str) -> list[str]:
"""Finds a url in a text blob."""
# findall() has been used
# with valid conditions for urls in string
- regex = r"(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'\".,<>?«»""'']))"
+ regex = (
+ r"(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'\".,<>?«»"
+ "'']))"
+ )
url = re.findall(regex, text)
return [x[0] for x in url]
diff --git a/src/dispatch/plugins/dispatch_google/groups/plugin.py b/src/dispatch/plugins/dispatch_google/groups/plugin.py
index aac6dedac399..ea6d41fc98fc 100644
--- a/src/dispatch/plugins/dispatch_google/groups/plugin.py
+++ b/src/dispatch/plugins/dispatch_google/groups/plugin.py
@@ -26,7 +26,9 @@
retry=retry_if_exception_type(TryAgain),
wait=wait_exponential(multiplier=1, min=2, max=5),
)
-def make_call(client: Any, func: Any, delay: int | None = None, propagate_errors: bool = False, **kwargs):
+def make_call(
+ client: Any, func: Any, delay: int | None = None, propagate_errors: bool = False, **kwargs
+):
"""Make an google client api call."""
try:
data = getattr(client, func)(**kwargs).execute()
diff --git a/src/dispatch/plugins/dispatch_slack/case/interactive.py b/src/dispatch/plugins/dispatch_slack/case/interactive.py
index 0c79c900ba6e..04052f28c0ac 100644
--- a/src/dispatch/plugins/dispatch_slack/case/interactive.py
+++ b/src/dispatch/plugins/dispatch_slack/case/interactive.py
@@ -287,7 +287,7 @@ def handle_update_case_command(
),
case_visibility_select(
initial_option={"text": case.visibility, "value": case.visibility},
- )
+ ),
]
modal = Modal(
diff --git a/src/dispatch/plugins/dispatch_slack/case/messages.py b/src/dispatch/plugins/dispatch_slack/case/messages.py
index 12d5cbdb0f93..481841806969 100644
--- a/src/dispatch/plugins/dispatch_slack/case/messages.py
+++ b/src/dispatch/plugins/dispatch_slack/case/messages.py
@@ -1,5 +1,7 @@
import logging
from typing import NamedTuple
+import html
+import re
from blockkit import (
@@ -46,21 +48,61 @@
def map_priority_color(color: str) -> str:
- """Maps a priority color to its corresponding emoji symbol."""
- if not color:
- return ""
+ """
+ Returns the first slack-compatible priority color for the given color.
- # TODO we should probably restrict the possible colors to make this work
- priority_color_mapping = {
- "#9e9e9e": "⚪",
- "#8bc34a": "🟢",
- "#ffeb3b": "🟡",
- "#ff9800": "🟠",
- "#f44336": "🔴",
- "#9c27b0": "🟣",
+ Args:
+ color (str): RGB Hex color string.
+
+ Returns:
+ str: Slack Color string.
+ """
+ color_mappings = {
+ "red": ":red_circle:",
+ "orange": ":orange_circle:",
+ "amber": ":yellow_circle:",
+ "green": ":green_circle:",
+ "blue": ":blue_circle:",
+ "purple": ":purple_circle:",
+ "grey": ":grey_circle:",
+ "gray": ":grey_circle:",
}
- return priority_color_mapping.get(color.lower(), "")
+ for mapping_color in color_mappings:
+ if mapping_color in color.lower():
+ return color_mappings[mapping_color]
+
+ return ":blue_circle:"
+
+
+def html_to_plain_text(html_content: str) -> str:
+ """
+ Convert HTML content to plain text for Slack messages.
+
+ Args:
+ html_content (str): HTML content to convert
+
+ Returns:
+ str: Plain text content
+ """
+ if not html_content:
+ return ""
+
+ # First decode any HTML entities
+ text = html.unescape(html_content)
+
+ # Remove HTML tags but preserve line breaks
+ text = re.sub(r"
", "\n", text)
+ text = re.sub(r"
]*>", "", text)
+ text = re.sub(r"?[^>]+>", "", text)
+
+ # Clean up extra whitespace
+ text = re.sub(r"\n\s*\n\s*\n", "\n\n", text) # Multiple newlines to double
+ text = re.sub(r"[ \t]+", " ", text) # Multiple spaces/tabs to single space
+ text = text.strip()
+
+ return text
def create_case_message(case: Case, channel_id: str) -> list[Block]:
@@ -142,11 +184,13 @@ def create_case_message(case: Case, channel_id: str) -> list[Block]:
]
)
elif case.status == CaseStatus.closed:
+ # Convert HTML resolution to plain text for Slack display
+ resolution_text = html_to_plain_text(case.resolution) if case.resolution else ""
blocks.extend(
[
Section(text=f"*Resolution reason* \n {case.resolution_reason}"),
Section(
- text=f"*Resolution description* \n {case.resolution}"[:MAX_SECTION_TEXT_LENGTH]
+ text=f"*Resolution description* \n {resolution_text}"[:MAX_SECTION_TEXT_LENGTH]
),
Actions(
elements=[
diff --git a/src/dispatch/plugins/dispatch_slack/fields.py b/src/dispatch/plugins/dispatch_slack/fields.py
index 888e6cd505cd..95e4b8f0c104 100644
--- a/src/dispatch/plugins/dispatch_slack/fields.py
+++ b/src/dispatch/plugins/dispatch_slack/fields.py
@@ -696,7 +696,7 @@ def case_visibility_select(
"""Creates a case visibility select."""
visibility = [
{"text": Visibility.restricted, "value": Visibility.restricted},
- {"text": Visibility.open, "value": Visibility.open}
+ {"text": Visibility.open, "value": Visibility.open},
]
return static_select_block(
diff --git a/src/dispatch/plugins/dispatch_slack/messaging.py b/src/dispatch/plugins/dispatch_slack/messaging.py
index e11662b8ca50..c12906aeb78b 100644
--- a/src/dispatch/plugins/dispatch_slack/messaging.py
+++ b/src/dispatch/plugins/dispatch_slack/messaging.py
@@ -143,18 +143,18 @@ def get_incident_conversation_command_message(
def build_command_error_message(payload: dict, error: Any) -> str:
- message = f"""Unfortunately we couldn't run `{payload['command']}` due to the following reason: {str(error)} """
+ message = f"""Unfortunately we couldn't run `{payload["command"]}` due to the following reason: {str(error)} """
return message
def build_role_error_message(payload: dict) -> str:
- message = f"""I see you tried to run `{payload['command']}`. This is a sensitive command and cannot be run with the incident role you are currently assigned."""
+ message = f"""I see you tried to run `{payload["command"]}`. This is a sensitive command and cannot be run with the incident role you are currently assigned."""
return message
def build_context_error_message(payload: dict, error: Any) -> str:
message = (
- f"""I see you tried to run `{payload['command']}` in an non-incident conversation. Incident-specific commands can only be run in incident conversations.""" # command_context_middleware()
+ f"""I see you tried to run `{payload["command"]}` in an non-incident conversation. Incident-specific commands can only be run in incident conversations.""" # command_context_middleware()
if payload.get("command")
else str(error) # everything else
)
diff --git a/src/dispatch/plugins/dispatch_slack/plugin.py b/src/dispatch/plugins/dispatch_slack/plugin.py
index 5e10e0fafc2d..1871dda26e6a 100644
--- a/src/dispatch/plugins/dispatch_slack/plugin.py
+++ b/src/dispatch/plugins/dispatch_slack/plugin.py
@@ -442,7 +442,10 @@ def fetch_events(
activity = event_instance.fetch_activity(client, subject, oldest)
return activity
except Exception as e:
- logger.exception("An error occurred while fetching incident or case events from the Slack plugin.", exc_info=e)
+ logger.exception(
+ "An error occurred while fetching incident or case events from the Slack plugin.",
+ exc_info=e,
+ )
raise
def get_conversation_replies(self, conversation_id: str, thread_ts: str) -> list[str]:
diff --git a/src/dispatch/report/flows.py b/src/dispatch/report/flows.py
index 6b473658d041..e1faf6a8d86d 100644
--- a/src/dispatch/report/flows.py
+++ b/src/dispatch/report/flows.py
@@ -98,12 +98,14 @@ def create_executive_report(
incident = incident_service.get(db_session=db_session, incident_id=incident_id)
if not incident.incident_type.executive_template_document:
- raise ValidationError([
- {
- "msg": "No executive report template defined.",
- "loc": "executive_template_document",
- }
- ])
+ raise ValidationError(
+ [
+ {
+ "msg": "No executive report template defined.",
+ "loc": "executive_template_document",
+ }
+ ]
+ )
# we fetch all previous executive reports
executive_reports = get_all_by_incident_id_and_type(
diff --git a/src/dispatch/report/service.py b/src/dispatch/report/service.py
index 8299ddf0e226..5db8209560c1 100644
--- a/src/dispatch/report/service.py
+++ b/src/dispatch/report/service.py
@@ -1,4 +1,3 @@
-
from .enums import ReportTypes
from .models import Report, ReportCreate, ReportUpdate
diff --git a/src/dispatch/search/fulltext/__init__.py b/src/dispatch/search/fulltext/__init__.py
index 4619b5d89a37..18b510c9bff6 100644
--- a/src/dispatch/search/fulltext/__init__.py
+++ b/src/dispatch/search/fulltext/__init__.py
@@ -2,6 +2,7 @@
Originally authored by:
https://github.com/kvesteri/sqlalchemy-searchable/blob/master/sqlalchemy_searchable
"""
+
import os
from functools import reduce
@@ -55,9 +56,11 @@ def search(query, search_query, vector=None, regconfig=None, sort=False):
# Get the entity class from the query in a SQLAlchemy 2.x compatible way
try:
# For SQLAlchemy 2.x
- entity = query.column_descriptions[0]['entity']
+ entity = query.column_descriptions[0]["entity"]
except (AttributeError, IndexError, KeyError):
- raise ValueError("Could not determine entity class from query. Please provide vector explicitly.") from None
+ raise ValueError(
+ "Could not determine entity class from query. Please provide vector explicitly."
+ ) from None
search_vectors = inspect_search_vectors(entity)
vector = search_vectors[0]
diff --git a/src/dispatch/search/models.py b/src/dispatch/search/models.py
index 7cb40670f16a..f758284766f5 100644
--- a/src/dispatch/search/models.py
+++ b/src/dispatch/search/models.py
@@ -16,9 +16,11 @@
from dispatch.team.models import TeamContactRead
from dispatch.term.models import TermRead
+
# Pydantic models...
class SearchBase(DispatchBase):
"""Base model for search queries."""
+
query: str | None = None
@@ -28,6 +30,7 @@ class SearchRequest(SearchBase):
class ContentResponse(DispatchBase):
"""Model for search content response."""
+
documents: list[DocumentRead] | None = Field(default_factory=list, alias="Document")
incidents: list[IncidentRead] | None = Field(default_factory=list, alias="Incident")
tasks: list[TaskRead] | None = Field(default_factory=list, alias="Task")
@@ -37,7 +40,9 @@ class ContentResponse(DispatchBase):
sources: list[SourceRead] | None = Field(default_factory=list, alias="Source")
queries: list[QueryRead] | None = Field(default_factory=list, alias="Query")
teams: list[TeamContactRead] | None = Field(default_factory=list, alias="TeamContact")
- individuals: list[IndividualContactRead] | None = Field(default_factory=list, alias="IndividualContact")
+ individuals: list[IndividualContactRead] | None = Field(
+ default_factory=list, alias="IndividualContact"
+ )
services: list[ServiceRead] | None = Field(default_factory=list, alias="Service")
cases: list[CaseRead] | None = Field(default_factory=list, alias="Case")
model_config: ClassVar[ConfigDict] = ConfigDict(populate_by_name=True)
@@ -45,5 +50,6 @@ class ContentResponse(DispatchBase):
class SearchResponse(DispatchBase):
"""Model for a search response."""
+
query: str | None = None
results: ContentResponse
diff --git a/src/dispatch/search_filter/service.py b/src/dispatch/search_filter/service.py
index 67ef809671cd..e57ffabcf142 100644
--- a/src/dispatch/search_filter/service.py
+++ b/src/dispatch/search_filter/service.py
@@ -1,4 +1,3 @@
-
from sqlalchemy_filters import apply_filters
from dispatch.database.core import Base, get_class_by_tablename, get_table_name_by_class_instance
diff --git a/src/dispatch/service/service.py b/src/dispatch/service/service.py
index 6352d1f1dd4b..5b3ecd43887c 100644
--- a/src/dispatch/service/service.py
+++ b/src/dispatch/service/service.py
@@ -1,4 +1,3 @@
-
from pydantic import ValidationError
from dispatch.plugin import service as plugin_service
@@ -39,14 +38,16 @@ def get_by_name_or_raise(*, db_session, project_id, service_in: ServiceRead) ->
source = get_by_name(db_session=db_session, project_id=project_id, name=service_in.name)
if not source:
- raise ValidationError([
- {
- "loc": ("service",),
- "msg": f"Service not found: {service_in.name}",
- "type": "value_error",
- "input": service_in.name,
- }
- ])
+ raise ValidationError(
+ [
+ {
+ "loc": ("service",),
+ "msg": f"Service not found: {service_in.name}",
+ "type": "value_error",
+ "input": service_in.name,
+ }
+ ]
+ )
return source
diff --git a/src/dispatch/static/dispatch/.npmrc b/src/dispatch/static/dispatch/.npmrc
index ff753f54526a..74d1d2708e10 100644
--- a/src/dispatch/static/dispatch/.npmrc
+++ b/src/dispatch/static/dispatch/.npmrc
@@ -1,2 +1 @@
//registry.npmjs.org/:_authToken=${FORMKIT_ENTERPRISE_TOKEN}
-
diff --git a/src/dispatch/static/dispatch/components.d.ts b/src/dispatch/static/dispatch/components.d.ts
index cd9ce5015d8e..b4e4bf790339 100644
--- a/src/dispatch/static/dispatch/components.d.ts
+++ b/src/dispatch/static/dispatch/components.d.ts
@@ -8,11 +8,9 @@ export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
AdminLayout: typeof import('./src/components/layouts/AdminLayout.vue')['default']
- AnimatedNumber: typeof import("./src/components/AnimatedNumber.vue")["default"]
AppDrawer: typeof import('./src/components/AppDrawer.vue')['default']
AppToolbar: typeof import('./src/components/AppToolbar.vue')['default']
AutoComplete: typeof import('./src/components/AutoComplete.vue')['default']
- Avatar: typeof import("./src/components/Avatar.vue")["default"]
BaseCombobox: typeof import('./src/components/BaseCombobox.vue')['default']
BasicLayout: typeof import('./src/components/layouts/BasicLayout.vue')['default']
ColorPickerInput: typeof import('./src/components/ColorPickerInput.vue')['default']
@@ -25,17 +23,15 @@ declare module '@vue/runtime-core' {
DMenu: typeof import('./src/components/DMenu.vue')['default']
DTooltip: typeof import('./src/components/DTooltip.vue')['default']
GenaiAnalysisDisplay: typeof import('./src/components/GenaiAnalysisDisplay.vue')['default']
+ HtmlRenderer: typeof import('./src/components/HtmlRenderer.vue')['default']
IconPickerInput: typeof import('./src/components/IconPickerInput.vue')['default']
InfoWidget: typeof import('./src/components/InfoWidget.vue')['default']
Loading: typeof import('./src/components/Loading.vue')['default']
LockButton: typeof import('./src/components/LockButton.vue')['default']
MonacoEditor: typeof import('./src/components/MonacoEditor.vue')['default']
NotificationSnackbarsWrapper: typeof import('./src/components/NotificationSnackbarsWrapper.vue')['default']
- PageHeader: typeof import("./src/components/PageHeader.vue")["default"]
- ParticipantAutoComplete: typeof import("./src/components/ParticipantAutoComplete.vue")["default"]
ParticipantSelect: typeof import('./src/components/ParticipantSelect.vue')['default']
PreciseDateTimePicker: typeof import('./src/components/PreciseDateTimePicker.vue')['default']
- ProjectAutoComplete: typeof import("./src/components/ProjectAutoComplete.vue")["default"]
Refresh: typeof import('./src/components/Refresh.vue')['default']
RichEditor: typeof import('./src/components/RichEditor.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
@@ -43,93 +39,6 @@ declare module '@vue/runtime-core' {
SavingState: typeof import('./src/components/SavingState.vue')['default']
SearchPopover: typeof import('./src/components/SearchPopover.vue')['default']
SettingsBreadcrumbs: typeof import('./src/components/SettingsBreadcrumbs.vue')['default']
- ShepherdStep: typeof import("./src/components/ShepherdStep.vue")["default"]
- ShpherdStep: typeof import("./src/components/ShpherdStep.vue")["default"]
StatWidget: typeof import('./src/components/StatWidget.vue')['default']
- SubjectLastUpdated: typeof import("./src/components/SubjectLastUpdated.vue")["default"]
- TimePicker: typeof import("./src/components/TimePicker.vue")["default"]
- VAlert: typeof import("vuetify/lib")["VAlert"]
- VApp: typeof import("vuetify/lib")["VApp"]
- VAppBar: typeof import("vuetify/lib")["VAppBar"]
- VAutocomplete: typeof import("vuetify/lib")["VAutocomplete"]
- VAvatar: typeof import("vuetify/lib")["VAvatar"]
- VBadge: typeof import("vuetify/lib")["VBadge"]
- VBottomSheet: typeof import("vuetify/lib")["VBottomSheet"]
- VBreadcrumbs: typeof import("vuetify/lib")["VBreadcrumbs"]
- VBreadcrumbsItem: typeof import("vuetify/lib")["VBreadcrumbsItem"]
- VBtn: typeof import("vuetify/lib")["VBtn"]
- VCard: typeof import("vuetify/lib")["VCard"]
- VCardActions: typeof import("vuetify/lib")["VCardActions"]
- VCardSubtitle: typeof import("vuetify/lib")["VCardSubtitle"]
- VCardText: typeof import("vuetify/lib")["VCardText"]
- VCardTitle: typeof import("vuetify/lib")["VCardTitle"]
- VCheckbox: typeof import("vuetify/lib")["VCheckbox"]
- VChip: typeof import("vuetify/lib")["VChip"]
- VChipGroup: typeof import("vuetify/lib")["VChipGroup"]
- VCol: typeof import("vuetify/lib")["VCol"]
- VColorPicker: typeof import("vuetify/lib")["VColorPicker"]
- VCombobox: typeof import("vuetify/lib")["VCombobox"]
- VContainer: typeof import("vuetify/lib")["VContainer"]
- VDataTable: typeof import("vuetify/lib")["VDataTable"]
- VDatePicker: typeof import("vuetify/lib")["VDatePicker"]
- VDialog: typeof import("vuetify/lib")["VDialog"]
- VDivider: typeof import("vuetify/lib")["VDivider"]
- VExpandTransition: typeof import("vuetify/lib")["VExpandTransition"]
- VExpansionPanel: typeof import("vuetify/lib")["VExpansionPanel"]
- VExpansionPanelContent: typeof import("vuetify/lib")["VExpansionPanelContent"]
- VExpansionPanelHeader: typeof import("vuetify/lib")["VExpansionPanelHeader"]
- VExpansionPanels: typeof import("vuetify/lib")["VExpansionPanels"]
- VFlex: typeof import("vuetify/lib")["VFlex"]
- VForm: typeof import("vuetify/lib")["VForm"]
- VHover: typeof import("vuetify/lib")["VHover"]
- VIcon: typeof import("vuetify/lib")["VIcon"]
- VItem: typeof import("vuetify/lib")["VItem"]
- VLayout: typeof import("vuetify/lib")["VLayout"]
- VLazy: typeof import("vuetify/lib")["VLazy"]
- VList: typeof import("vuetify/lib")["VList"]
- VListGroup: typeof import("vuetify/lib")["VListGroup"]
- VListItem: typeof import("vuetify/lib")["VListItem"]
- VListItemAction: typeof import("vuetify/lib")["VListItemAction"]
- VListItemAvatar: typeof import("vuetify/lib")["VListItemAvatar"]
- VListItemContent: typeof import("vuetify/lib")["VListItemContent"]
- VListItemGroup: typeof import("vuetify/lib")["VListItemGroup"]
- VListItemIcon: typeof import("vuetify/lib")["VListItemIcon"]
- VListItemSubtitle: typeof import("vuetify/lib")["VListItemSubtitle"]
- VListItemTitle: typeof import("vuetify/lib")["VListItemTitle"]
- VMain: typeof import("vuetify/lib")["VMain"]
- VMenu: typeof import("vuetify/lib")["VMenu"]
- VNavigationDrawer: typeof import("vuetify/lib")["VNavigationDrawer"]
- VProgressLinear: typeof import("vuetify/lib")["VProgressLinear"]
- VRadio: typeof import("vuetify/lib")["VRadio"]
- VRadioGroup: typeof import("vuetify/lib")["VRadioGroup"]
- VRow: typeof import("vuetify/lib")["VRow"]
- VSelect: typeof import("vuetify/lib")["VSelect"]
- VSheet: typeof import("vuetify/lib")["VSheet"]
- VSimpleCheckbox: typeof import("vuetify/lib")["VSimpleCheckbox"]
- VSnackbar: typeof import("vuetify/lib")["VSnackbar"]
- VSpacer: typeof import("vuetify/lib")["VSpacer"]
- VStepper: typeof import("vuetify/lib")["VStepper"]
- VStepperContent: typeof import("vuetify/lib")["VStepperContent"]
- VStepperHeader: typeof import("vuetify/lib")["VStepperHeader"]
- VStepperItems: typeof import("vuetify/lib")["VStepperItems"]
- VStepperStep: typeof import("vuetify/lib")["VStepperStep"]
- VSubheader: typeof import("vuetify/lib")["VSubheader"]
- VSwitch: typeof import("vuetify/lib")["VSwitch"]
- VSystemBar: typeof import("vuetify/lib")["VSystemBar"]
- VTab: typeof import("vuetify/lib")["VTab"]
- VTabItem: typeof import("vuetify/lib")["VTabItem"]
- VTabs: typeof import("vuetify/lib")["VTabs"]
- VTabsItems: typeof import("vuetify/lib")["VTabsItems"]
- VTextarea: typeof import("vuetify/lib")["VTextarea"]
- VTextArea: typeof import("vuetify/lib")["VTextArea"]
- VTextField: typeof import("vuetify/lib")["VTextField"]
- VTimeline: typeof import("vuetify/lib")["VTimeline"]
- VTimelineItem: typeof import("vuetify/lib")["VTimelineItem"]
- VTimePicker: typeof import("vuetify/lib")["VTimePicker"]
- VToolbarItems: typeof import("vuetify/lib")["VToolbarItems"]
- VToolbarTitle: typeof import("vuetify/lib")["VToolbarTitle"]
- VTooltip: typeof import("vuetify/lib")["VTooltip"]
- VWindow: typeof import("vuetify/lib")["VWindow"]
- VWindowItem: typeof import("vuetify/lib")["VWindowItem"]
}
}
diff --git a/src/dispatch/static/dispatch/jsconfig.json b/src/dispatch/static/dispatch/jsconfig.json
index e8b3494e07b5..b8d6842d7fad 100644
--- a/src/dispatch/static/dispatch/jsconfig.json
+++ b/src/dispatch/static/dispatch/jsconfig.json
@@ -4,4 +4,4 @@
"@/*": ["./src/*"]
}
}
-}
\ No newline at end of file
+}
diff --git a/src/dispatch/static/dispatch/public/static/robots.txt b/src/dispatch/static/dispatch/public/static/robots.txt
index 6f27bb66a346..eb0536286f30 100644
--- a/src/dispatch/static/dispatch/public/static/robots.txt
+++ b/src/dispatch/static/dispatch/public/static/robots.txt
@@ -1,2 +1,2 @@
User-agent: *
-Disallow:
\ No newline at end of file
+Disallow:
diff --git a/src/dispatch/static/dispatch/src/case/CaseAttributesDrawer.vue b/src/dispatch/static/dispatch/src/case/CaseAttributesDrawer.vue
index 3ce037f86e5b..631dc467bb9e 100644
--- a/src/dispatch/static/dispatch/src/case/CaseAttributesDrawer.vue
+++ b/src/dispatch/static/dispatch/src/case/CaseAttributesDrawer.vue
@@ -11,6 +11,7 @@ import DTooltip from "@/components/DTooltip.vue"
import ParticipantSearchPopover from "@/participant/ParticipantSearchPopover.vue"
import ProjectSearchPopover from "@/project/ProjectSearchPopover.vue"
import TagSearchPopover from "@/tag/TagSearchPopover.vue"
+import RichEditor from "@/components/RichEditor.vue"
import { useSavingState } from "@/composables/useSavingState"
// Define the props
diff --git a/src/dispatch/static/dispatch/src/case/ClosedDialog.vue b/src/dispatch/static/dispatch/src/case/ClosedDialog.vue
index 660765180fd1..4e89e9ddc2ef 100644
--- a/src/dispatch/static/dispatch/src/case/ClosedDialog.vue
+++ b/src/dispatch/static/dispatch/src/case/ClosedDialog.vue
@@ -19,11 +19,18 @@
/>