diff --git a/src/dispatch/case/views.py b/src/dispatch/case/views.py index 3dd85650838d..056efdc39c85 100644 --- a/src/dispatch/case/views.py +++ b/src/dispatch/case/views.py @@ -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__) @@ -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) diff --git a/src/dispatch/document/service.py b/src/dispatch/document/service.py index acf48aa55a89..21deb5866fe5 100644 --- a/src/dispatch/document/service.py +++ b/src/dispatch/document/service.py @@ -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 diff --git a/src/dispatch/individual/models.py b/src/dispatch/individual/models.py index 270421139865..61be564e5643 100644 --- a/src/dispatch/individual/models.py +++ b/src/dispatch/individual/models.py @@ -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")), @@ -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") diff --git a/src/dispatch/individual/service.py b/src/dispatch/individual/service.py index 65eec1466e36..0a664db10a8a 100644 --- a/src/dispatch/individual/service.py +++ b/src/dispatch/individual/service.py @@ -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 @@ -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, diff --git a/src/dispatch/plugin/models.py b/src/dispatch/plugin/models.py index 3bacb4cddc35..3008ab5e54f8 100644 --- a/src/dispatch/plugin/models.py +++ b/src/dispatch/plugin/models.py @@ -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}" @@ -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}" diff --git a/src/dispatch/plugins/dispatch_core/plugin.py b/src/dispatch/plugins/dispatch_core/plugin.py index 11841a370837..43acb74e1e7d 100644 --- a/src/dispatch/plugins/dispatch_core/plugin.py +++ b/src/dispatch/plugins/dispatch_core/plugin.py @@ -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") @@ -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( @@ -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) @@ -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): @@ -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, @@ -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( @@ -499,6 +505,7 @@ class DispatchParticipantResolverPlugin(ParticipantPlugin): author = "Netflix" author_url = "https://github.com/netflix/dispatch.git" + configuration_schema = None def get( self, diff --git a/src/dispatch/project/models.py b/src/dispatch/project/models.py index 81be1877d9d9..91af903a3077 100644 --- a/src/dispatch/project/models.py +++ b/src/dispatch/project/models.py @@ -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): @@ -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") @@ -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"}) @@ -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 @@ -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 @@ -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] = []