Skip to content
This repository was archived by the owner on Sep 3, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
- name: Install playwright browsers
run: npx playwright install --with-deps chromium
- name: Setup sample database
run: dispatch database restore --dump-file data/dispatch-sample-data.dump && dispatch database upgrade
run: dispatch database restore --dump-file data/dispatch-sample-data.dump --skip-check && dispatch database upgrade
- name: Run tests
run: npx playwright test --project=chromium
- uses: actions/upload-artifact@v4
Expand Down
175 changes: 90 additions & 85 deletions data/dispatch-sample-data.dump

Large diffs are not rendered by default.

73 changes: 59 additions & 14 deletions src/dispatch/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,13 +266,55 @@ def dispatch_database():
pass


def prompt_for_confirmation(command: str) -> bool:
"""Prompts the user for database details."""
from dispatch.config import DATABASE_HOSTNAME, DATABASE_NAME
from sqlalchemy_utils import database_exists

database_hostname = click.prompt(
f"Please enter the database hostname (env = {DATABASE_HOSTNAME})"
)
if database_hostname != DATABASE_HOSTNAME:
click.secho(
f"ERROR: You cannot {command} a database with a different hostname.",
fg="red",
)
return False
if database_hostname != "localhost":
click.secho(
f"Warning: You are about to {command} a remote database.",
fg="yellow",
)
database_name = click.prompt(f"Please enter the database name (env = {DATABASE_NAME})")
if database_name != DATABASE_NAME:
click.secho(
f"ERROR: You cannot {command} a database with a different name.",
fg="red",
)
return False
sqlalchemy_database_uri = f"postgresql+psycopg2://{config._DATABASE_CREDENTIAL_USER}:{config._QUOTED_DATABASE_PASSWORD}@{database_hostname}:{config.DATABASE_PORT}/{database_name}"

if database_exists(str(sqlalchemy_database_uri)):
if click.confirm(
f"Are you sure you want to {command} database: '{database_hostname}:{database_name}'?"
):
return True
else:
click.secho(f"Database '{database_hostname}:{database_name}' does not exist!!!", fg="red")
return False


@dispatch_database.command("init")
def database_init():
"""Initializes a new database."""
click.echo("Initializing new database...")
from .database.core import engine
from .database.manage import init_database

if not prompt_for_confirmation("init"):
click.secho("Aborting database initialization.", fg="red")
return

init_database(engine)
click.secho("Success.", fg="green")

Expand All @@ -283,7 +325,8 @@ def database_init():
default="dispatch-backup.dump",
help="Path to a PostgreSQL text format dump file.",
)
def restore_database(dump_file):
@click.option("--skip-check", is_flag=True, help="Skip confirmation check if flag is set.")
def restore_database(dump_file, skip_check):
"""Restores the database via psql."""
from sh import ErrorReturnCode_1, createdb, psql

Expand All @@ -294,6 +337,10 @@ def restore_database(dump_file):
DATABASE_PORT,
)

if not skip_check and not prompt_for_confirmation("restore"):
click.secho("Aborting database restore.", fg="red")
return

username, password = str(DATABASE_CREDENTIALS).split(":")

try:
Expand Down Expand Up @@ -366,22 +413,20 @@ def dump_database(dump_file):
@dispatch_database.command("drop")
def drop_database():
"""Drops all data in database."""
from sqlalchemy_utils import database_exists, drop_database
from sqlalchemy_utils import drop_database

database_hostname = click.prompt(
f"Please enter the database hostname (env = {config.DATABASE_HOSTNAME})"
if not prompt_for_confirmation("drop"):
click.secho("Aborting database drop.", fg="red")
return

sqlalchemy_database_uri = (
f"postgresql+psycopg2://{config._DATABASE_CREDENTIAL_USER}:"
f"{config._QUOTED_DATABASE_PASSWORD}@{config.DATABASE_HOSTNAME}:"
f"{config.DATABASE_PORT}/{config.DATABASE_NAME}"
)
database_name = click.prompt(f"Please enter the database name (env = {config.DATABASE_NAME})")
sqlalchemy_database_uri = f"postgresql+psycopg2://{config._DATABASE_CREDENTIAL_USER}:{config._QUOTED_DATABASE_PASSWORD}@{database_hostname}:{config.DATABASE_PORT}/{database_name}"

if database_exists(str(sqlalchemy_database_uri)):
if click.confirm(
f"Are you sure you want to drop database: '{database_hostname}:{database_name}'?"
):
drop_database(str(sqlalchemy_database_uri))
click.secho("Success.", fg="green")
else:
click.secho(f"Database '{database_hostname}:{database_name}' does not exist!!!", fg="red")
drop_database(str(sqlalchemy_database_uri))
click.secho("Success.", fg="green")


@dispatch_database.command("upgrade")
Expand Down
17 changes: 15 additions & 2 deletions src/dispatch/incident/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Models for incident resources in the Dispatch application."""

from collections import Counter, defaultdict
from datetime import datetime

Expand Down Expand Up @@ -238,6 +239,7 @@ def participant_observer(self, participants):

class ProjectRead(DispatchBase):
"""Pydantic model for reading a project resource."""

id: PrimaryKey | None
name: NameStr
color: str | None
Expand All @@ -248,12 +250,14 @@ class ProjectRead(DispatchBase):

class CaseRead(DispatchBase):
"""Pydantic model for reading a case resource."""

id: PrimaryKey
name: NameStr | None


class TaskRead(DispatchBase):
"""Pydantic model for reading a task resource."""

id: PrimaryKey
assignees: list[ParticipantRead | None] = []
created_at: datetime | None
Expand All @@ -268,6 +272,7 @@ class TaskRead(DispatchBase):

class TaskReadMinimal(DispatchBase):
"""Pydantic model for reading a minimal task resource."""

id: PrimaryKey
description: str | None = None
status: TaskStatus = TaskStatus.open
Expand All @@ -276,6 +281,7 @@ class TaskReadMinimal(DispatchBase):
# Pydantic models...
class IncidentBase(DispatchBase):
"""Base Pydantic model for incident resources."""

title: str
description: str
resolution: str | None
Expand All @@ -301,8 +307,9 @@ def description_required(cls, v: str) -> str:

class IncidentCreate(IncidentBase):
"""Pydantic model for creating an incident resource."""
commander: ParticipantUpdate | None
commander_email: str | None

commander: ParticipantUpdate | None = None
commander_email: str | None = None
incident_priority: IncidentPriorityCreate | None
incident_severity: IncidentSeverityCreate | None
incident_type: IncidentTypeCreate | None
Expand All @@ -313,12 +320,14 @@ class IncidentCreate(IncidentBase):

class IncidentReadBasic(DispatchBase):
"""Pydantic model for reading a basic incident resource."""

id: PrimaryKey
name: NameStr | None


class IncidentReadMinimal(IncidentBase):
"""Pydantic model for reading a minimal incident resource."""

id: PrimaryKey
closed_at: datetime | None = None
commander: ParticipantReadMinimal | None
Expand Down Expand Up @@ -348,6 +357,7 @@ class IncidentReadMinimal(IncidentBase):

class IncidentUpdate(IncidentBase):
"""Pydantic model for updating an incident resource."""

cases: list[CaseRead] | None = []
commander: ParticipantUpdate | None
delay_executive_report_reminder: datetime | None = None
Expand Down Expand Up @@ -384,6 +394,7 @@ def find_exclusive(cls, v):

class IncidentRead(IncidentBase):
"""Pydantic model for reading an incident resource."""

id: PrimaryKey
cases: list[CaseRead] | None = []
closed_at: datetime | None = None
Expand Down Expand Up @@ -424,11 +435,13 @@ class IncidentRead(IncidentBase):

class IncidentExpandedPagination(Pagination):
"""Pydantic model for paginated expanded incident results."""

itemsPerPage: int
page: int
items: list[IncidentRead] = []


class IncidentPagination(Pagination):
"""Pydantic model for paginated incident results."""

items: list[IncidentReadMinimal] = []
19 changes: 18 additions & 1 deletion src/dispatch/incident/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@
from dispatch.event.models import EventCreateMinimal, EventUpdate
from dispatch.incident.enums import IncidentStatus
from dispatch.individual.models import IndividualContactRead
from dispatch.individual.service import get_or_create
from dispatch.models import OrganizationSlug, PrimaryKey
from dispatch.participant.models import ParticipantUpdate
from dispatch.project import service as project_service
from dispatch.report import flows as report_flows
from dispatch.report.models import ExecutiveReportCreate, TacticalReportCreate

Expand Down Expand Up @@ -120,8 +122,23 @@ def create_incident(
):
"""Creates a new incident."""
if not incident_in.reporter:
# Ensure the individual exists, create if not
if incident_in.project is None:
raise HTTPException(
status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=[{"msg": "Project must be set to create reporter individual."}],
)
# Fetch the full DB project instance
project = project_service.get_by_name_or_default(
db_session=db_session, project_in=incident_in.project
)
individual = get_or_create(
db_session=db_session,
email=current_user.email,
project=project,
)
incident_in.reporter = ParticipantUpdate(
individual=IndividualContactRead(email=current_user.email)
individual=IndividualContactRead(id=individual.id, email=individual.email)
)
incident = create(db_session=db_session, incident_in=incident_in)

Expand Down
68 changes: 34 additions & 34 deletions src/dispatch/static/dispatch/components.d.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,51 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/core/pull/3399
import "@vue/runtime-core"
import '@vue/runtime-core'

export {}

declare module "@vue/runtime-core" {
declare module '@vue/runtime-core' {
export interface GlobalComponents {
AdminLayout: typeof import("./src/components/layouts/AdminLayout.vue")["default"]
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"]
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"]
DashboardLayout: typeof import("./src/components/layouts/DashboardLayout.vue")["default"]
DateChipGroupRelative: typeof import("./src/components/DateChipGroupRelative.vue")["default"]
DateTimePicker: typeof import("./src/components/DateTimePicker.vue")["default"]
DateTimePickerMenu: typeof import("./src/components/DateTimePickerMenu.vue")["default"]
DateWindowInput: typeof import("./src/components/DateWindowInput.vue")["default"]
DefaultLayout: typeof import("./src/components/layouts/DefaultLayout.vue")["default"]
DMenu: typeof import("./src/components/DMenu.vue")["default"]
DTooltip: typeof import("./src/components/DTooltip.vue")["default"]
GenaiAnalysisDisplay: typeof import("./src/components/GenaiAnalysisDisplay.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"]
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']
DashboardLayout: typeof import('./src/components/layouts/DashboardLayout.vue')['default']
DateChipGroupRelative: typeof import('./src/components/DateChipGroupRelative.vue')['default']
DateTimePicker: typeof import('./src/components/DateTimePicker.vue')['default']
DateTimePickerMenu: typeof import('./src/components/DateTimePickerMenu.vue')['default']
DateWindowInput: typeof import('./src/components/DateWindowInput.vue')['default']
DefaultLayout: typeof import('./src/components/layouts/DefaultLayout.vue')['default']
DMenu: typeof import('./src/components/DMenu.vue')['default']
DTooltip: typeof import('./src/components/DTooltip.vue')['default']
GenaiAnalysisDisplay: typeof import('./src/components/GenaiAnalysisDisplay.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"]
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"]
RouterView: typeof import("vue-router")["RouterView"]
SavingState: typeof import("./src/components/SavingState.vue")["default"]
SearchPopover: typeof import("./src/components/SearchPopover.vue")["default"]
SettingsBreadcrumbs: typeof import("./src/components/SettingsBreadcrumbs.vue")["default"]
Refresh: typeof import('./src/components/Refresh.vue')['default']
RichEditor: typeof import('./src/components/RichEditor.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
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"]
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"]
Expand Down
4 changes: 3 additions & 1 deletion src/dispatch/ticket/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

class Ticket(Base, ResourceMixin):
"""SQLAlchemy model for ticket resources."""

id = Column(Integer, primary_key=True)
incident_id = Column(Integer, ForeignKey("incident.id", ondelete="CASCADE"))
case_id = Column(Integer, ForeignKey("case.id", ondelete="CASCADE"))
Expand All @@ -32,7 +33,8 @@ class TicketUpdate(TicketBase):

class TicketRead(TicketBase):
"""Pydantic model for reading a ticket resource."""
description: str | None = None

description: str | None = TICKET_DESCRIPTION

@field_validator("description", mode="before")
@classmethod
Expand Down
Loading