Skip to content
This repository was archived by the owner on Sep 3, 2025. It is now read-only.

Commit 867679f

Browse files
authored
add base concept of entities to signals (#2977)
* add base concenpt of entities to signals * Add entity model to store type results and add entity case tab * Add missing import and remove unused import * Remove debug statements and sleep from find_entities function * Fix entity_type update service and fix signal instance popover in table * fix service tests, signal_instance id should be uuid not integer * Remove debug console.log statement * move find_entities function to entity service * Update comment * Remove global find from front-end, add regex help message about groups * Address @kglisson comments * Make case edit sheet wider to give tabs some room to breathe
1 parent 24fdb20 commit 867679f

34 files changed

+2233
-119
lines changed

src/dispatch/api.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
from dispatch.data.source.views import router as source_router
2222
from dispatch.definition.views import router as definition_router
2323
from dispatch.document.views import router as document_router
24+
from dispatch.entity.views import router as entity_router
25+
from dispatch.entity_type.views import router as entity_type_router
2426
from dispatch.feedback.views import router as feedback_router
2527
from dispatch.incident.priority.views import router as incident_priority_router
2628
from dispatch.incident.severity.views import router as incident_severity_router
@@ -131,6 +133,12 @@ def get_organization_path(organization: OrganizationSlug):
131133
authenticated_organization_api_router.include_router(
132134
document_router, prefix="/documents", tags=["documents"]
133135
)
136+
authenticated_organization_api_router.include_router(
137+
entity_router, prefix="/entity", tags=["entities"]
138+
)
139+
authenticated_organization_api_router.include_router(
140+
entity_type_router, prefix="/entity_type", tags=["entity_types"]
141+
)
134142
authenticated_organization_api_router.include_router(tag_router, prefix="/tags", tags=["tags"])
135143
authenticated_organization_api_router.include_router(
136144
tag_type_router, prefix="/tag_types", tags=["tag_types"]

src/dispatch/case/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from dispatch.database.core import Base
2626
from dispatch.document.models import Document, DocumentRead
2727
from dispatch.enums import Visibility
28+
from dispatch.entity.models import EntityRead
2829
from dispatch.event.models import EventRead
2930
from dispatch.group.models import Group, GroupRead
3031
from dispatch.incident.models import IncidentReadMinimal
@@ -165,6 +166,7 @@ class SignalRead(DispatchBase):
165166

166167
class SignalInstanceRead(DispatchBase):
167168
signal: SignalRead
169+
entities: Optional[List[EntityRead]] = []
168170
tags: Optional[List[TagRead]] = []
169171
raw: Any
170172
fingerprint: str

src/dispatch/case/service.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -195,12 +195,13 @@ def create(*, db_session, case_in: CaseCreate, current_user: DispatchUser = None
195195
)
196196

197197
# add reporter
198-
participant_flows.add_participant(
199-
case_in.reporter.individual.email,
200-
case,
201-
db_session,
202-
role=ParticipantRoleType.reporter,
203-
)
198+
if case_in.reporter:
199+
participant_flows.add_participant(
200+
case_in.reporter.individual.email,
201+
case,
202+
db_session,
203+
role=ParticipantRoleType.reporter,
204+
)
204205

205206
return case
206207

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""Adds entity and entity type tables and associations
2+
3+
Revision ID: 8746b4e292d2
4+
Revises: 941efd922446
5+
Create Date: 2023-02-09 23:18:11.326027
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
from sqlalchemy.dialects.postgresql import TSVECTOR, UUID
11+
12+
# revision identifiers, used by Alembic.
13+
revision = "8746b4e292d2"
14+
down_revision = "941efd922446"
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# ### commands auto generated by Alembic - please adjust!
21+
22+
# EntityType
23+
op.create_table(
24+
"entity_type",
25+
sa.Column("id", sa.Integer(), nullable=False),
26+
sa.Column("name", sa.String(), nullable=True),
27+
sa.Column("description", sa.String(), nullable=True),
28+
sa.Column("field", sa.String(), nullable=True),
29+
sa.Column("regular_expression", sa.String(), nullable=True),
30+
sa.Column("global_find", sa.Boolean(), nullable=True),
31+
sa.Column("enabled", sa.Boolean(), nullable=True),
32+
sa.Column("search_vector", TSVECTOR, nullable=True),
33+
sa.Column("project_id", sa.Integer(), nullable=True),
34+
sa.Column("updated_at", sa.DateTime(), nullable=True),
35+
sa.Column("created_at", sa.DateTime(), nullable=True),
36+
sa.ForeignKeyConstraint(["project_id"], ["project.id"], ondelete="CASCADE"),
37+
sa.PrimaryKeyConstraint("id"),
38+
sa.UniqueConstraint("name", "project_id"),
39+
)
40+
op.create_table(
41+
"assoc_signal_entity_types",
42+
sa.Column("signal_id", sa.Integer(), nullable=False),
43+
sa.Column("entity_type_id", sa.Integer(), nullable=False),
44+
sa.ForeignKeyConstraint(["signal_id"], ["signal.id"], ondelete="CASCADE"),
45+
sa.ForeignKeyConstraint(["entity_type_id"], ["entity_type.id"], ondelete="CASCADE"),
46+
sa.PrimaryKeyConstraint("signal_id", "entity_type_id"),
47+
)
48+
op.create_index(
49+
"entity_type_search_vector_idx",
50+
"entity_type",
51+
["search_vector"],
52+
unique=False,
53+
postgresql_using="gin",
54+
)
55+
56+
# Entity
57+
op.create_table(
58+
"entity",
59+
sa.Column("id", sa.Integer(), nullable=False),
60+
sa.Column("name", sa.String(), nullable=True),
61+
sa.Column("description", sa.String(), nullable=True),
62+
sa.Column("value", sa.String(), nullable=True),
63+
sa.Column("source", sa.Boolean(), nullable=True),
64+
sa.Column("entity_type_id", sa.Integer(), nullable=False),
65+
sa.Column("search_vector", TSVECTOR, nullable=True),
66+
sa.Column("project_id", sa.Integer(), nullable=True),
67+
sa.Column("updated_at", sa.DateTime(), nullable=True),
68+
sa.Column("created_at", sa.DateTime(), nullable=True),
69+
sa.ForeignKeyConstraint(
70+
["entity_type_id"],
71+
["entity_type.id"],
72+
),
73+
sa.ForeignKeyConstraint(["project_id"], ["project.id"], ondelete="CASCADE"),
74+
sa.PrimaryKeyConstraint("id"),
75+
sa.UniqueConstraint("name", "project_id"),
76+
)
77+
op.create_index(
78+
"ix_entity_search_vector",
79+
"entity",
80+
["search_vector"],
81+
unique=False,
82+
postgresql_using="gin",
83+
)
84+
op.create_table(
85+
"assoc_signal_instance_entities",
86+
sa.Column("signal_instance_id", UUID(), nullable=False),
87+
sa.Column("entity_id", sa.Integer(), nullable=False),
88+
sa.ForeignKeyConstraint(["signal_instance_id"], ["signal_instance.id"], ondelete="CASCADE"),
89+
sa.ForeignKeyConstraint(["entity_id"], ["entity.id"], ondelete="CASCADE"),
90+
sa.PrimaryKeyConstraint("signal_instance_id", "entity_id"),
91+
)
92+
93+
94+
def downgrade():
95+
# ### commands auto generated by Alembic - please adjust! ###
96+
op.drop_index("ix_entity_search_vector", table_name="entity")
97+
op.drop_table("entity")
98+
op.drop_table("entity_type")
99+
op.drop_index("entity_type_search_vector_idx", table_name="entity", postgresql_using="gin")
100+
op.drop_table("assoc_signal_entity_types")
101+
op.drop_table("assoc_signal_instance_entity_types")
102+
# ### end Alembic commands ###

src/dispatch/entity/__init__.py

Whitespace-only changes.

src/dispatch/entity/models.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from typing import Optional, List
2+
from pydantic import Field
3+
4+
from sqlalchemy import Column, Integer, String, ForeignKey
5+
from sqlalchemy.orm import relationship
6+
from sqlalchemy.sql.schema import UniqueConstraint
7+
from sqlalchemy_utils import TSVectorType
8+
9+
from dispatch.database.core import Base
10+
from dispatch.models import DispatchBase, TimeStampMixin, ProjectMixin, PrimaryKey
11+
from dispatch.project.models import ProjectRead
12+
from dispatch.entity_type.models import (
13+
EntityTypeCreate,
14+
EntityTypeRead,
15+
EntityTypeReadMinimal,
16+
EntityTypeUpdate,
17+
)
18+
19+
20+
class Entity(Base, TimeStampMixin, ProjectMixin):
21+
__table_args__ = (UniqueConstraint("name", "project_id"),)
22+
23+
# Columns
24+
id = Column(Integer, primary_key=True)
25+
name = Column(String)
26+
description = Column(String)
27+
value = Column(String)
28+
source = Column(String)
29+
30+
# Relationships
31+
entity_type_id = Column(Integer, ForeignKey("entity_type.id"), nullable=False)
32+
entity_type = relationship("EntityType", backref="entity")
33+
34+
# the catalog here is simple to help matching "named entities"
35+
search_vector = Column(
36+
TSVectorType(
37+
"name",
38+
"description",
39+
weights={"name": "A", "description": "B"},
40+
regconfig="pg_catalog.simple",
41+
)
42+
)
43+
44+
45+
# Pydantic models
46+
class EntityBase(DispatchBase):
47+
name: Optional[str] = Field(None, nullable=True)
48+
source: Optional[str] = Field(None, nullable=True)
49+
value: Optional[str] = Field(None, nullable=True)
50+
description: Optional[str] = Field(None, nullable=True)
51+
52+
53+
class EntityCreate(EntityBase):
54+
id: Optional[PrimaryKey]
55+
entity_type: EntityTypeCreate
56+
project: ProjectRead
57+
58+
59+
class EntityUpdate(EntityBase):
60+
id: Optional[PrimaryKey]
61+
entity_type: Optional[EntityTypeUpdate]
62+
63+
64+
class EntityRead(EntityBase):
65+
id: PrimaryKey
66+
entity_type: Optional[EntityTypeRead]
67+
project: ProjectRead
68+
69+
70+
class EntityReadMinimal(DispatchBase):
71+
id: PrimaryKey
72+
name: Optional[str] = Field(None, nullable=True)
73+
source: Optional[str] = Field(None, nullable=True)
74+
value: Optional[str] = Field(None, nullable=True)
75+
description: Optional[str] = Field(None, nullable=True)
76+
entity_type: Optional[EntityTypeReadMinimal]
77+
78+
79+
class EntityPagination(DispatchBase):
80+
items: List[EntityRead]
81+
total: int

0 commit comments

Comments
 (0)