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
32 changes: 17 additions & 15 deletions src/dispatch/case/views.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,46 @@
import json
import logging
from typing import Annotated

import json

from starlette.requests import Request
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, status

from sqlalchemy.exc import IntegrityError
from starlette.requests import Request

# NOTE: define permissions before enabling the code block below
from dispatch.auth.permissions import (
CaseEditPermission,
CaseJoinPermission,
PermissionsDependency,
CaseViewPermission,
PermissionsDependency,
)
from dispatch.auth.service import CurrentUser
from dispatch.case.enums import CaseStatus
from dispatch.common.utils.views import create_pydantic_include
from dispatch.database.core import DbSession
from dispatch.database.service import CommonParameters, search_filter_sort_paginate
from dispatch.models import OrganizationSlug, PrimaryKey
from dispatch.incident.models import IncidentCreate, IncidentRead
from dispatch.incident import service as incident_service
from dispatch.participant.models import ParticipantUpdate, ParticipantRead, ParticipantReadMinimal
from dispatch.incident.models import IncidentCreate, IncidentRead
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 ParticipantRead, ParticipantReadMinimal, ParticipantUpdate
from dispatch.project import service as project_service

from .flows import (
case_add_or_reactivate_participant_flow,
case_closed_create_flow,
case_create_conversation_flow,
case_create_resources_flow,
case_delete_flow,
case_escalated_create_flow,
case_to_incident_endpoint_escalate_flow,
case_new_create_flow,
case_to_incident_endpoint_escalate_flow,
case_triage_create_flow,
case_update_flow,
case_create_conversation_flow,
case_create_resources_flow,
get_case_participants_flow,
)
from .models import Case, CaseCreate, CasePagination, CaseRead, CaseUpdate, CaseExpandedPagination
from .service import create, delete, get, update, get_participants

from .models import Case, CaseCreate, CaseExpandedPagination, CasePagination, CaseRead, CaseUpdate
from .service import create, delete, get, get_participants, update

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -155,10 +153,14 @@ def create_case(
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=case_in.project
)
individual = get_or_create(
db_session=db_session,
email=current_user.email,
project=case_in.project,
project=project,
)
case_in.reporter = ParticipantUpdate(
individual=IndividualContactRead(id=individual.id, email=individual.email)
Expand Down
2 changes: 1 addition & 1 deletion src/dispatch/document/service.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from pydantic.error_wrappers import ValidationError
from datetime import datetime
from pydantic import ValidationError

from dispatch.enums import DocumentResourceReferenceTypes, DocumentResourceTemplateTypes
from dispatch.project import service as project_service
Expand Down
6 changes: 3 additions & 3 deletions src/dispatch/individual/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
)

# Association tables for many to many relationships
assoc_individual_filters = Table(
"assoc_individual_filters",
assoc_individual_contact_filters = Table(
"assoc_individual_contact_filters",
Base.metadata,
Column("individual_contact_id", Integer, ForeignKey("individual_contact.id", ondelete="CASCADE")),
Column("search_filter_id", Integer, ForeignKey("search_filter.id", ondelete="CASCADE")),
Expand All @@ -47,7 +47,7 @@ class IndividualContact(Base, ContactMixin, ProjectMixin, TimeStampMixin):
service_feedback = relationship("ServiceFeedback", backref="individual")

filters = relationship(
"SearchFilter", secondary=assoc_individual_filters, backref="individuals"
"SearchFilter", secondary=assoc_individual_contact_filters, backref="individuals"
)
team_contact_id = Column(Integer, ForeignKey("team_contact.id"))
team_contact = relationship("TeamContact", backref="individuals")
Expand Down
13 changes: 10 additions & 3 deletions src/dispatch/individual/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from sqlalchemy.orm import Session

from dispatch.plugin.models import PluginInstance
from dispatch.project.models import Project
from dispatch.project.models import Project, ProjectRead
from dispatch.plugin import service as plugin_service
from dispatch.project import service as project_service
from dispatch.search_filter import service as search_filter_service
Expand Down Expand Up @@ -97,15 +97,22 @@ def get_or_create(
kwargs["name"] = individual_info.get("fullname", email.split("@")[0].capitalize())
kwargs["weblink"] = individual_info.get("weblink", "")

# Use Pydantic's model_validate to convert SQLAlchemy Project to ProjectRead
project_read = ProjectRead.model_validate(project)
if project_read.annual_employee_cost is None:
project_read.annual_employee_cost = 50000
if project_read.business_year_hours is None:
project_read.business_year_hours = 2080

if not individual_contact:
# we create a new contact
individual_contact_in = IndividualContactCreate(**kwargs, project=project)
individual_contact_in = IndividualContactCreate(**kwargs, project=project_read)
individual_contact = create(
db_session=db_session, individual_contact_in=individual_contact_in
)
else:
# we update the existing contact
individual_contact_in = IndividualContactUpdate(**kwargs, project=project)
individual_contact_in = IndividualContactUpdate(**kwargs, project=project_read)
individual_contact = update(
db_session=db_session,
individual_contact=individual_contact,
Expand Down
8 changes: 6 additions & 2 deletions src/dispatch/plugin/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ def configuration_schema(self):
"""Renders the plugin's schema to JSON Schema."""
try:
plugin = plugins.get(self.slug)
return plugin.configuration_schema.schema()
if getattr(plugin, "configuration_schema", None) is not None:
return plugin.configuration_schema.schema()
return None
except Exception as e:
logger.warning(
f"Error trying to load configuration_schema for plugin with slug {self.slug}: {e}"
Expand Down Expand Up @@ -120,7 +122,9 @@ def configuration_schema(self):
"""Renders the plugin's schema to JSON Schema."""
try:
plugin = plugins.get(self.plugin.slug)
return plugin.configuration_schema.schema()
if getattr(plugin, "configuration_schema", None) is not None:
return plugin.configuration_schema.schema()
return None
except Exception as e:
logger.warning(
f"Error trying to load plugin {self.plugin.title} {self.plugin.description} with error {e}"
Expand Down
7 changes: 7 additions & 0 deletions src/dispatch/plugins/dispatch_core/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class BasicAuthProviderPlugin(AuthenticationProviderPlugin):

author = "Netflix"
author_url = "https://github.com/netflix/dispatch.git"
configuration_schema = None

def get_current_user(self, request: Request, **kwargs):
authorization: str = request.headers.get("Authorization")
Expand Down Expand Up @@ -105,6 +106,7 @@ class PKCEAuthProviderPlugin(AuthenticationProviderPlugin):

author = "Netflix"
author_url = "https://github.com/netflix/dispatch.git"
configuration_schema = None

def get_current_user(self, request: Request, **kwargs):
credentials_exception = HTTPException(
Expand Down Expand Up @@ -157,6 +159,7 @@ class HeaderAuthProviderPlugin(AuthenticationProviderPlugin):

author = "Filippo Giunchedi"
author_url = "https://github.com/filippog"
configuration_schema = None

def get_current_user(self, request: Request, **kwargs):
value: str = request.headers.get(DISPATCH_AUTHENTICATION_PROVIDER_HEADER_NAME)
Expand All @@ -176,6 +179,7 @@ class AwsAlbAuthProviderPlugin(AuthenticationProviderPlugin):

author = "ManyPets"
author_url = "https://manypets.com/"
configuration_schema = None

@cached(cache=TTLCache(maxsize=1024, ttl=DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_PUBLIC_KEY_CACHE_SECONDS))
def get_public_key(self, kid: str, region: str):
Expand Down Expand Up @@ -364,6 +368,7 @@ class DispatchMfaPlugin(MultiFactorAuthenticationPlugin):

author = "Netflix"
author_url = "https://github.com/netflix/dispatch.git"
configuration_schema = None

def wait_for_challenge(
self,
Expand Down Expand Up @@ -478,6 +483,7 @@ class DispatchContactPlugin(ContactPlugin):

author = "Netflix"
author_url = "https://github.com/netflix/dispatch.git"
configuration_schema = None

def get(self, email, db_session=None):
individual = individual_service.get_by_email_and_project(
Expand All @@ -499,6 +505,7 @@ class DispatchParticipantResolverPlugin(ParticipantPlugin):

author = "Netflix"
author_url = "https://github.com/netflix/dispatch.git"
configuration_schema = None

def get(
self,
Expand Down
45 changes: 21 additions & 24 deletions src/dispatch/project/models.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
from pydantic import EmailStr
from pydantic import ConfigDict, EmailStr, Field
from slugify import slugify
from pydantic import Field

from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy import Column, Integer, String, ForeignKey, Boolean
from sqlalchemy.sql import false
from sqlalchemy.orm import relationship
from sqlalchemy.sql import false
from sqlalchemy_utils import TSVectorType

from dispatch.database.core import Base
from dispatch.models import DispatchBase, NameStr, PrimaryKey, Pagination

from dispatch.organization.models import Organization, OrganizationRead
from dispatch.incident.priority.models import (
IncidentPriority,
IncidentPriorityRead,
)
from dispatch.models import DispatchBase, NameStr, Pagination, PrimaryKey
from dispatch.organization.models import Organization, OrganizationRead


class Project(Base):
Expand All @@ -37,9 +34,7 @@ class Project(Base):
organization = relationship("Organization")

dispatch_user_project = relationship(
"DispatchUserProject",
cascade="all, delete-orphan",
overlaps="users"
"DispatchUserProject", cascade="all, delete-orphan", overlaps="users"
)

enabled = Column(Boolean, default=True, server_default="t")
Expand Down Expand Up @@ -81,7 +76,7 @@ class Project(Base):

@hybrid_property
def slug(self):
return slugify(self.name)
return slugify(str(self.name))

search_vector = Column(
TSVectorType("name", "description", weights={"name": "A", "description": "B"})
Expand All @@ -100,24 +95,24 @@ class Service(DispatchBase):
class ProjectBase(DispatchBase):
id: PrimaryKey | None
name: NameStr
display_name: str | None = Field("", nullable=False)
display_name: str | None = Field("")
owner_email: EmailStr | None = None
owner_conversation: str | None = None
annual_employee_cost: int | None
business_year_hours: int | None
annual_employee_cost: int | None = 50000
business_year_hours: int | None = 2080
description: str | None = None
default: bool = False
color: str | None = None
send_daily_reports: bool | None = Field(True, nullable=True)
send_weekly_reports: bool | None = Field(False, nullable=True)
send_daily_reports: bool | None = Field(True)
send_weekly_reports: bool | None = Field(False)
weekly_report_notification_id: int | None = None
enabled: bool | None = Field(True, nullable=True)
enabled: bool | None = Field(True)
storage_folder_one: str | None = None
storage_folder_two: str | None = None
storage_use_folder_one_as_primary: bool | None = Field(True, nullable=True)
storage_use_title: bool | None = Field(False, nullable=True)
allow_self_join: bool | None = Field(True, nullable=True)
select_commander_visibility: bool | None = Field(True, nullable=True)
storage_use_folder_one_as_primary: bool | None = Field(True)
storage_use_title: bool | None = Field(False)
allow_self_join: bool | None = Field(True)
select_commander_visibility: bool | None = Field(True)
report_incident_instructions: str | None = None
report_incident_title_hint: str | None = None
report_incident_description_hint: str | None = None
Expand All @@ -129,8 +124,8 @@ class ProjectCreate(ProjectBase):


class ProjectUpdate(ProjectBase):
send_daily_reports: bool | None = Field(True, nullable=True)
send_weekly_reports: bool | None = Field(False, nullable=True)
send_daily_reports: bool | None = Field(True)
send_weekly_reports: bool | None = Field(False)
weekly_report_notification_id: int | None = None
stable_priority_id: int | None
snooze_extension_oncall_service_id: int | None
Expand All @@ -140,6 +135,8 @@ class ProjectRead(ProjectBase):
id: PrimaryKey | None
stable_priority: IncidentPriorityRead | None = None

model_config = ConfigDict(from_attributes=True)


class ProjectPagination(Pagination):
items: list[ProjectRead] = []
Loading