Skip to content

Commit

Permalink
Merge branch 'master' into bugfix/use-service-id-for-service-feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
whitdog47 authored Oct 29, 2024
2 parents 18a0cd8 + 6b96299 commit 5f7aa8b
Show file tree
Hide file tree
Showing 29 changed files with 633 additions and 243 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
# Minimum code coverage per file
COVERAGE_SINGLE: 50
# Minimum total code coverage
COVERAGE_TOTAL: 56
COVERAGE_TOTAL: 55
runs-on: ubuntu-latest
services:
postgres:
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ default_language_version:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# ruff version.
rev: v0.6.4
rev: v0.7.0
hooks:
# Run the linter.
#
Expand All @@ -28,7 +28,7 @@ repos:

# Typos
- repo: https://github.com/crate-ci/typos
rev: v1.24.5
rev: v1.26.1
hooks:
- id: typos
exclude: ^(data/dispatch-sample-data.dump|src/dispatch/static/dispatch/src/|src/dispatch/database/revisions/)
Expand Down
6 changes: 3 additions & 3 deletions docs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions requirements-base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ six==1.16.0
# python-dateutil
# sqlalchemy-filters
# validators
slack-bolt==1.20.1
slack-bolt==1.21.2
# via -r requirements-base.in
slack-sdk==3.33.1
# via
Expand Down Expand Up @@ -496,7 +496,7 @@ wasabi==1.1.2
# weasel
weasel==0.3.4
# via spacy
werkzeug==3.0.3
werkzeug==3.0.6
# via schemathesis
wrapt==1.16.0
# via deprecated
Expand Down
8 changes: 4 additions & 4 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ click==8.1.7
# via
# -r requirements-dev.in
# black
coverage==7.6.3
coverage==7.6.4
# via -r requirements-dev.in
decorator==5.1.1
# via ipython
Expand All @@ -32,7 +32,7 @@ executing==2.0.1
# stack-data
factory-boy==3.3.1
# via -r requirements-dev.in
faker==30.6.0
faker==30.8.1
# via
# -r requirements-dev.in
# factory-boy
Expand All @@ -42,7 +42,7 @@ identify==2.5.33
# via pre-commit
iniconfig==2.0.0
# via pytest
ipython==8.28.0
ipython==8.29.0
# via -r requirements-dev.in
jedi==0.19.1
# via ipython
Expand Down Expand Up @@ -86,7 +86,7 @@ python-dateutil==2.9.0.post0
# via faker
pyyaml==6.0.1
# via pre-commit
ruff==0.6.9
ruff==0.7.1
# via -r requirements-dev.in
six==1.16.0
# via
Expand Down
35 changes: 20 additions & 15 deletions src/dispatch/case/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,45 +8,43 @@
from dispatch.case.messaging import send_case_welcome_participant_message
from dispatch.case.models import CaseRead
from dispatch.conversation import flows as conversation_flows
from dispatch.database.core import SessionLocal
from dispatch.decorators import background_task
from dispatch.document import flows as document_flows
from dispatch.enums import DocumentResourceTypes, Visibility, EventType
from dispatch.enums import DocumentResourceTypes, EventType, Visibility
from dispatch.event import service as event_service
from dispatch.group import flows as group_flows
from dispatch.group.enums import GroupAction, GroupType
from dispatch.incident import flows as incident_flows
from dispatch.incident import service as incident_service
from dispatch.incident.enums import IncidentStatus
from dispatch.incident.messaging import send_participant_announcement_message
from dispatch.incident.models import IncidentCreate, Incident
from dispatch.incident.type.models import IncidentType
from dispatch.incident.models import Incident, IncidentCreate
from dispatch.incident.priority.models import IncidentPriority
from dispatch.incident.type.models import IncidentType
from dispatch.individual.models import IndividualContactRead
from dispatch.models import OrganizationSlug, PrimaryKey
from dispatch.participant import flows as participant_flows
from dispatch.participant import service as participant_service
from dispatch.participant.models import ParticipantUpdate
from dispatch.participant_role import flows as role_flow
from dispatch.participant_role.models import ParticipantRoleType, ParticipantRole
from dispatch.participant_role.models import ParticipantRole, ParticipantRoleType
from dispatch.plugin import service as plugin_service
from dispatch.storage import flows as storage_flows
from dispatch.storage.enums import StorageAction
from dispatch.ticket import flows as ticket_flows

from .messaging import (
send_case_created_notifications,
send_case_update_notifications,
send_case_rating_feedback_message,
send_case_update_notifications,
)

from .models import Case, CaseStatus
from .service import get

log = logging.getLogger(__name__)


def get_case_participants_flow(case: Case, db_session: SessionLocal):
def get_case_participants_flow(case: Case, db_session: Session):
"""Get additional case participants based on priority, type and description."""
individual_contacts = []
team_contacts = []
Expand Down Expand Up @@ -337,17 +335,21 @@ def case_update_flow(
# we get the case
case = get(db_session=db_session, case_id=case_id)

if reporter_email:
# we run the case assign role flow for the reporter
if not case:
log.warning(f"Case with id {case_id} not found.")
return

if reporter_email and case.reporter and reporter_email != case.reporter.individual.email:
# we run the case assign role flow for the reporter if it changed
case_assign_role_flow(
case_id=case.id,
participant_email=reporter_email,
participant_role=ParticipantRoleType.reporter,
db_session=db_session,
)

if assignee_email:
# we run the case assign role flow for the assignee
if assignee_email and case.assignee and assignee_email != case.assignee.individual.email:
# we run the case assign role flow for the assignee if it changed
case_assign_role_flow(
case_id=case.id,
participant_email=assignee_email,
Expand Down Expand Up @@ -375,15 +377,15 @@ def case_update_flow(

if case.tactical_group:
# we update the tactical group
if reporter_email:
if reporter_email and case.reporter and reporter_email != case.reporter.individual.email:
group_flows.update_group(
subject=case,
group=case.tactical_group,
group_action=GroupAction.add_member,
group_member=reporter_email,
db_session=db_session,
)
if assignee_email:
if assignee_email and case.assignee and assignee_email != case.assignee.individual.email:
group_flows.update_group(
subject=case,
group=case.tactical_group,
Expand All @@ -406,7 +408,7 @@ def case_update_flow(
send_case_update_notifications(case, previous_case, db_session)


def case_delete_flow(case: Case, db_session: SessionLocal):
def case_delete_flow(case: Case, db_session: Session):
"""Runs the case delete flow."""
# we delete the external ticket
if case.ticket:
Expand Down Expand Up @@ -489,6 +491,9 @@ def case_closed_status_flow(case: Case, db_session=None):
if not storage_plugin:
return

# we update the ticket
ticket_flows.update_case_ticket(case=case, db_session=db_session)

# Open document access if configured
if storage_plugin.configuration.open_on_close:
for document in case.documents:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Adds configuration to the Dispatch Ticket PluginInstance
Revision ID: 24322617ce9a
Revises: 3c49f62d7914
Create Date: 2024-10-25 15:15:38.078421
"""

from alembic import op
from pydantic import SecretStr, ValidationError
from pydantic.json import pydantic_encoder

from sqlalchemy import Column, Integer, ForeignKey, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, Session
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy_utils import StringEncryptedType
from sqlalchemy_utils.types.encrypted.encrypted_type import AesEngine
from dispatch.config import DISPATCH_ENCRYPTION_KEY

# revision identifiers, used by Alembic.
revision = "24322617ce9a"
down_revision = "3c49f62d7914"
branch_labels = None
depends_on = None

Base = declarative_base()


def show_secrets_encoder(obj):
if isinstance(obj, SecretStr):
return obj.get_secret_value()
else:
return pydantic_encoder(obj)


def migrate_config(instances, slug, config):
for instance in instances:
if slug == instance.plugin.slug:
instance.configuration = config


class Plugin(Base):
__tablename__ = "plugin"
__table_args__ = {"schema": "dispatch_core"}
id = Column(Integer, primary_key=True)
slug = Column(String, unique=True)


class PluginInstance(Base):
__tablename__ = "plugin_instance"
id = Column(Integer, primary_key=True)
_configuration = Column(
StringEncryptedType(key=str(DISPATCH_ENCRYPTION_KEY), engine=AesEngine, padding="pkcs5")
)
plugin_id = Column(Integer, ForeignKey(Plugin.id))
plugin = relationship(Plugin, backref="instances")

@hybrid_property
def configuration(self):
"""Property that correctly returns a plugins configuration object."""
pass

@configuration.setter
def configuration(self, configuration):
"""Property that correctly sets a plugins configuration object."""
if configuration:
self._configuration = configuration.json(encoder=show_secrets_encoder)


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
from dispatch.plugins.dispatch_core.config import DispatchTicketConfiguration

bind = op.get_bind()
session = Session(bind=bind)

instances = session.query(PluginInstance).all()

try:
dispatch_ticket_config = DispatchTicketConfiguration(
use_incident_name=False,
)

migrate_config(instances, "dispatch-ticket", dispatch_ticket_config)

except ValidationError:
print(
"Skipping automatic migration of Dispatch ticket plugin, if you are using the Dispatch ticket plugin, please manually migrate."
)

session.commit()
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
56 changes: 54 additions & 2 deletions src/dispatch/feedback/incident/messaging.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import logging
from typing import List

from dispatch.database.core import SessionLocal
from sqlalchemy.orm import Session

from dispatch.messaging.strings import (
INCIDENT_FEEDBACK_DAILY_REPORT,
CASE_FEEDBACK_DAILY_REPORT,
MessageType,
)
from dispatch.plugin import service as plugin_service
Expand All @@ -15,7 +17,7 @@


def send_incident_feedback_daily_report(
commander_email: str, feedback: List[Feedback], project_id: int, db_session: SessionLocal
commander_email: str, feedback: List[Feedback], project_id: int, db_session: Session
):
"""Sends an incident feedback daily report to all incident commanders who received feedback."""
plugin = plugin_service.get_active_instance(
Expand Down Expand Up @@ -62,3 +64,53 @@ def send_incident_feedback_daily_report(
log.error(f"Error in sending {notification_text} email to {commander_email}: {e}")

log.debug(f"Incident feedback daily report sent to {commander_email}.")


def send_case_feedback_daily_report(
assignee_email: str, feedback: List[Feedback], project_id: int, db_session: Session
):
"""Sends an case feedback daily report to all case assignees who received feedback."""
plugin = plugin_service.get_active_instance(
db_session=db_session, project_id=project_id, plugin_type="email"
)

if not plugin:
log.warning("Case feedback daily report not sent. Email plugin is not enabled.")
return

items = []
for piece in feedback:
participant = piece.participant.individual.name if piece.participant else "Anonymous"
items.append(
{
"name": piece.case.name,
"title": piece.case.title,
"rating": piece.rating,
"feedback": piece.feedback,
"participant": participant,
"created_at": piece.created_at,
}
)

name = subject = notification_text = "Case Feedback Daily Report"
assignee_fullname = feedback[0].case.assignee.individual.name
assignee_weblink = feedback[0].case.assignee.individual.weblink

# Can raise exception "tenacity.RetryError: RetryError". (Email may still go through).
try:
plugin.instance.send(
assignee_email,
notification_text,
CASE_FEEDBACK_DAILY_REPORT,
MessageType.case_feedback_daily_report,
name=name,
subject=subject,
cc=plugin.project.owner_email,
items=items,
contact_fullname=assignee_fullname,
contact_weblink=assignee_weblink,
)
except Exception as e:
log.error(f"Error in sending {notification_text} email to {assignee_email}: {e}")

log.debug(f"Case feedback daily report sent to {assignee_email}.")
Loading

0 comments on commit 5f7aa8b

Please sign in to comment.