From ac5d618de694f79cb9427d5c8e249593e92a9385 Mon Sep 17 00:00:00 2001 From: Marc Vilanova Date: Wed, 21 Sep 2022 10:54:26 -0700 Subject: [PATCH 1/3] Show related case in the incident details tab --- src/dispatch/incident/models.py | 6 ++ ...eTypeCombobox.vue => CaseFilterSelect.vue} | 83 +++++++------------ .../dispatch/src/incident/DetailsTab.vue | 6 ++ .../static/dispatch/src/incident/store.js | 1 + 4 files changed, 44 insertions(+), 52 deletions(-) rename src/dispatch/static/dispatch/src/case/{CaseTypeCombobox.vue => CaseFilterSelect.vue} (62%) diff --git a/src/dispatch/incident/models.py b/src/dispatch/incident/models.py index b97283fcd7de..d364af1d9afc 100644 --- a/src/dispatch/incident/models.py +++ b/src/dispatch/incident/models.py @@ -216,6 +216,11 @@ class ProjectRead(DispatchBase): color: Optional[str] +class CaseRead(DispatchBase): + id: PrimaryKey + name: Optional[NameStr] + + # Pydantic models... class IncidentBase(DispatchBase): title: str @@ -290,6 +295,7 @@ def find_exclusive(cls, v): class IncidentReadMinimal(IncidentBase): id: PrimaryKey + case: Optional[CaseRead] closed_at: Optional[datetime] = None commander: Optional[ParticipantRead] commanders_location: Optional[str] diff --git a/src/dispatch/static/dispatch/src/case/CaseTypeCombobox.vue b/src/dispatch/static/dispatch/src/case/CaseFilterSelect.vue similarity index 62% rename from src/dispatch/static/dispatch/src/case/CaseTypeCombobox.vue rename to src/dispatch/static/dispatch/src/case/CaseFilterSelect.vue index 3d5c80ca1f79..821298aa8b96 100644 --- a/src/dispatch/static/dispatch/src/case/CaseTypeCombobox.vue +++ b/src/dispatch/static/dispatch/src/case/CaseFilterSelect.vue @@ -9,36 +9,34 @@ clearable deletable-chips hide-selected - item-text="id" + item-text="name" + item-value="id" multiple no-filter - v-model="caseType" + v-model="cases" > - @@ -117,6 +120,7 @@ import { ValidationProvider, extend } from "vee-validate" import { mapFields } from "vuex-map-fields" import { required } from "vee-validate/dist/rules" +import CaseFilterSelect from "@/case/CaseFilterSelect.vue" import DateTimePickerMenu from "@/components/DateTimePickerMenu.vue" import IncidentFilterCombobox from "@/incident/IncidentFilterCombobox.vue" import IncidentPrioritySelect from "@/incident_priority/IncidentPrioritySelect.vue" @@ -134,6 +138,7 @@ export default { name: "IncidentDetailsTab", components: { + CaseFilterSelect, DateTimePickerMenu, IncidentFilterCombobox, IncidentPrioritySelect, @@ -152,6 +157,7 @@ export default { }, computed: { + ...mapFields("incident", { relatedCase: "selected.case" }), ...mapFields("incident", [ "selected.commander", "selected.created_at", diff --git a/src/dispatch/static/dispatch/src/incident/store.js b/src/dispatch/static/dispatch/src/incident/store.js index b322ec6443fb..1f09d217d79d 100644 --- a/src/dispatch/static/dispatch/src/incident/store.js +++ b/src/dispatch/static/dispatch/src/incident/store.js @@ -7,6 +7,7 @@ import router from "@/router" const getDefaultSelectedState = () => { return { + case: null, commander: null, conference: null, conversation: null, From 28c63c2706403d2ad2cff9e357026a8de553d357 Mon Sep 17 00:00:00 2001 From: Marc Vilanova Date: Wed, 28 Sep 2022 14:32:12 -0700 Subject: [PATCH 2/3] Moves escalate option after view/edit --- src/dispatch/static/dispatch/src/case/Table.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/dispatch/static/dispatch/src/case/Table.vue b/src/dispatch/static/dispatch/src/case/Table.vue index 9358e21c716d..884049662639 100644 --- a/src/dispatch/static/dispatch/src/case/Table.vue +++ b/src/dispatch/static/dispatch/src/case/Table.vue @@ -5,8 +5,8 @@ - +
Cases
@@ -80,9 +80,6 @@ - - Escalate - + Escalate + Delete From d03944c9f3802e11bb78802d59ed3bc8045f9b12 Mon Sep 17 00:00:00 2001 From: Marc Vilanova Date: Fri, 30 Sep 2022 10:13:53 -0700 Subject: [PATCH 3/3] Alembic revision and other changes --- src/dispatch/case/models.py | 16 +- .../versions/2022-09-30_df200ca113f7.py | 41 +++++ src/dispatch/incident/models.py | 5 +- src/dispatch/incident/service.py | 9 +- .../dispatch/src/case/CaseFilterSelect.vue | 148 ------------------ .../dispatch/src/incident/DetailsTab.vue | 8 +- .../static/dispatch/src/incident/store.js | 2 +- 7 files changed, 69 insertions(+), 160 deletions(-) create mode 100644 src/dispatch/database/revisions/tenant/versions/2022-09-30_df200ca113f7.py delete mode 100644 src/dispatch/static/dispatch/src/case/CaseFilterSelect.vue diff --git a/src/dispatch/case/models.py b/src/dispatch/case/models.py index 6bca523354e9..30e39248ad3c 100644 --- a/src/dispatch/case/models.py +++ b/src/dispatch/case/models.py @@ -3,7 +3,6 @@ from typing import List, Optional from pydantic import validator -from dispatch.models import NameStr, PrimaryKey from sqlalchemy import ( Column, DateTime, @@ -29,6 +28,7 @@ from dispatch.incident.models import IncidentRead from dispatch.messaging.strings import CASE_RESOLUTION_DEFAULT from dispatch.models import DispatchBase, ProjectMixin, TimeStampMixin +from dispatch.models import NameStr, PrimaryKey from dispatch.storage.models import StorageRead from dispatch.tag.models import TagRead from dispatch.ticket.models import TicketRead @@ -45,6 +45,15 @@ PrimaryKeyConstraint("case_id", "tag_id"), ) +# Assoc table for cases and incidents +assoc_cases_incidents = Table( + "assoc_case_incidents", + Base.metadata, + Column("case_id", Integer, ForeignKey("case.id", ondelete="CASCADE")), + Column("incident_id", Integer, ForeignKey("incident.id", ondelete="CASCADE")), + PrimaryKeyConstraint("case_id", "incident_id"), +) + class Case(Base, TimeStampMixin, ProjectMixin): __table_args__ = (UniqueConstraint("name", "project_id"),) @@ -95,11 +104,12 @@ class Case(Base, TimeStampMixin, ProjectMixin): groups = relationship( "Group", backref="case", cascade="all, delete-orphan", foreign_keys=[Group.case_id] ) + + incidents = relationship("Incident", secondary=assoc_cases_incidents, backref="cases") + tactical_group_id = Column(Integer, ForeignKey("group.id")) tactical_group = relationship("Group", foreign_keys=[tactical_group_id]) - incidents = relationship("Incident", backref="case") - related_id = Column(Integer, ForeignKey("case.id")) related = relationship("Case", remote_side=[id], uselist=True, foreign_keys=[related_id]) diff --git a/src/dispatch/database/revisions/tenant/versions/2022-09-30_df200ca113f7.py b/src/dispatch/database/revisions/tenant/versions/2022-09-30_df200ca113f7.py new file mode 100644 index 000000000000..65971d2903a7 --- /dev/null +++ b/src/dispatch/database/revisions/tenant/versions/2022-09-30_df200ca113f7.py @@ -0,0 +1,41 @@ +"""Allows for many-to-many relationships for cases and incidents + +Revision ID: df200ca113f7 +Revises: e49209df586d +Create Date: 2022-09-30 10:02:46.584358 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "df200ca113f7" +down_revision = "e49209df586d" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "assoc_case_incidents", + sa.Column("case_id", sa.Integer(), nullable=False), + sa.Column("incident_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["case_id"], ["case.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["incident_id"], ["incident.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("case_id", "incident_id"), + ) + op.drop_constraint("incident_case_id_fkey", "incident", type_="foreignkey") + op.drop_column("incident", "case_id") + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "incident", sa.Column("case_id", sa.INTEGER(), autoincrement=False, nullable=True) + ) + op.create_foreign_key("incident_case_id_fkey", "incident", "case", ["case_id"], ["id"]) + op.drop_table("assoc_case_incidents") + # ### end Alembic commands ### diff --git a/src/dispatch/incident/models.py b/src/dispatch/incident/models.py index d364af1d9afc..598e5cd9ccdc 100644 --- a/src/dispatch/incident/models.py +++ b/src/dispatch/incident/models.py @@ -115,8 +115,6 @@ def last_executive_report(self): return sorted(self.executive_reports, key=lambda r: r.created_at)[-1] # resources - case_id = Column(Integer, ForeignKey("case.id")) - incident_costs = relationship( "IncidentCost", backref="incident", @@ -266,6 +264,7 @@ class IncidentCreate(IncidentBase): class IncidentUpdate(IncidentBase): + cases: Optional[List[CaseRead]] = [] commander: Optional[ParticipantUpdate] duplicates: Optional[List[IncidentReadNested]] = [] incident_costs: Optional[List[IncidentCostUpdate]] = [] @@ -295,7 +294,7 @@ def find_exclusive(cls, v): class IncidentReadMinimal(IncidentBase): id: PrimaryKey - case: Optional[CaseRead] + cases: Optional[List[CaseRead]] closed_at: Optional[datetime] = None commander: Optional[ParticipantRead] commanders_location: Optional[str] diff --git a/src/dispatch/incident/service.py b/src/dispatch/incident/service.py index 90a03ef0adb5..b9f534b93440 100644 --- a/src/dispatch/incident/service.py +++ b/src/dispatch/incident/service.py @@ -10,9 +10,10 @@ from typing import List, Optional from pydantic.error_wrappers import ErrorWrapper, ValidationError +from dispatch.case import service as case_service from dispatch.database.core import SessionLocal -from dispatch.exceptions import NotFoundError from dispatch.event import service as event_service +from dispatch.exceptions import NotFoundError from dispatch.incident_cost import service as incident_cost_service from dispatch.incident_priority import service as incident_priority_service from dispatch.incident_role.service import resolve_role @@ -269,6 +270,10 @@ def update(*, db_session, incident: Incident, incident_in: IncidentUpdate) -> In incident_priority_in=incident_in.incident_priority, ) + cases = [] + for c in incident_in.cases: + cases.append(case_service.get(db_session=db_session, case_id=c.id)) + tags = [] for t in incident_in.tags: tags.append(tag_service.get_or_create(db_session=db_session, tag_in=t)) @@ -292,6 +297,7 @@ def update(*, db_session, incident: Incident, incident_in: IncidentUpdate) -> In update_data = incident_in.dict( skip_defaults=True, exclude={ + "cases", "commander", "duplicates", "incident_costs", @@ -309,6 +315,7 @@ def update(*, db_session, incident: Incident, incident_in: IncidentUpdate) -> In for field in update_data.keys(): setattr(incident, field, update_data[field]) + incident.cases = cases incident.duplicates = duplicates incident.incident_costs = incident_costs incident.incident_priority = incident_priority diff --git a/src/dispatch/static/dispatch/src/case/CaseFilterSelect.vue b/src/dispatch/static/dispatch/src/case/CaseFilterSelect.vue deleted file mode 100644 index 821298aa8b96..000000000000 --- a/src/dispatch/static/dispatch/src/case/CaseFilterSelect.vue +++ /dev/null @@ -1,148 +0,0 @@ - - - diff --git a/src/dispatch/static/dispatch/src/incident/DetailsTab.vue b/src/dispatch/static/dispatch/src/incident/DetailsTab.vue index 59a61e09535d..d9fc0587fd64 100644 --- a/src/dispatch/static/dispatch/src/incident/DetailsTab.vue +++ b/src/dispatch/static/dispatch/src/incident/DetailsTab.vue @@ -109,7 +109,7 @@ - + @@ -120,7 +120,7 @@ import { ValidationProvider, extend } from "vee-validate" import { mapFields } from "vuex-map-fields" import { required } from "vee-validate/dist/rules" -import CaseFilterSelect from "@/case/CaseFilterSelect.vue" +import CaseFilterCombobox from "@/case/CaseFilterCombobox.vue" import DateTimePickerMenu from "@/components/DateTimePickerMenu.vue" import IncidentFilterCombobox from "@/incident/IncidentFilterCombobox.vue" import IncidentPrioritySelect from "@/incident_priority/IncidentPrioritySelect.vue" @@ -138,7 +138,7 @@ export default { name: "IncidentDetailsTab", components: { - CaseFilterSelect, + CaseFilterCombobox, DateTimePickerMenu, IncidentFilterCombobox, IncidentPrioritySelect, @@ -157,8 +157,8 @@ export default { }, computed: { - ...mapFields("incident", { relatedCase: "selected.case" }), ...mapFields("incident", [ + "selected.cases", "selected.commander", "selected.created_at", "selected.description", diff --git a/src/dispatch/static/dispatch/src/incident/store.js b/src/dispatch/static/dispatch/src/incident/store.js index 8baff4ae560e..c9eeca525323 100644 --- a/src/dispatch/static/dispatch/src/incident/store.js +++ b/src/dispatch/static/dispatch/src/incident/store.js @@ -7,7 +7,7 @@ import router from "@/router" const getDefaultSelectedState = () => { return { - case: null, + cases: [], commander: null, conference: null, conversation: null,