oncall-engine/engine/apps/alerts/models/resolution_note.py

208 lines
6.8 KiB
Python
Raw Permalink Normal View History

import typing
import humanize
from django.conf import settings
from django.core.validators import MinLengthValidator
from django.db import models
from django.utils import timezone
from rest_framework.fields import DateTimeField
from apps.slack.slack_formatter import SlackFormatter
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length
from common.utils import clean_markup
if typing.TYPE_CHECKING:
from apps.alerts.models import AlertGroup
from apps.slack.models import SlackChannel
from apps.user_management.models import User
def generate_public_primary_key_for_resolution_note():
prefix = "M"
new_public_primary_key = generate_public_primary_key(prefix)
failure_counter = 0
while ResolutionNote.objects.filter(public_primary_key=new_public_primary_key).exists():
new_public_primary_key = increase_public_primary_key_length(
failure_counter=failure_counter, prefix=prefix, model_name="ResolutionNote"
)
failure_counter += 1
return new_public_primary_key
class ResolutionNoteSlackMessageQueryset(models.QuerySet):
def delete(self):
resolution_note = self.get_resolution_note()
if resolution_note:
resolution_note.delete()
super().delete()
class ResolutionNoteSlackMessage(models.Model):
alert_group: "AlertGroup"
resolution_note: typing.Optional["ResolutionNote"]
slack_channel: typing.Optional["SlackChannel"]
alert_group = models.ForeignKey(
"alerts.AlertGroup",
on_delete=models.CASCADE,
related_name="resolution_note_slack_messages",
)
user = models.ForeignKey(
"user_management.User",
null=True,
on_delete=models.SET_NULL,
related_name="authored_resolution_note_slack_messages",
)
added_by_user = models.ForeignKey(
"user_management.User",
null=True,
on_delete=models.SET_NULL,
related_name="added_resolution_note_slack_messages",
)
text = models.TextField(max_length=3000, default=None, null=True)
slack_channel = models.ForeignKey(
"slack.SlackChannel",
null=True,
default=None,
on_delete=models.SET_NULL,
related_name="+",
)
ts = models.CharField(max_length=100, null=True, default=None)
thread_ts = models.CharField(max_length=100, null=True, default=None)
permalink = models.CharField(max_length=250, null=True, default=None)
added_to_resolution_note = models.BooleanField(default=False)
posted_by_bot = models.BooleanField(default=False)
class Meta:
unique_together = ("thread_ts", "ts")
indexes = [
models.Index(fields=["ts", "thread_ts", "alert_group_id"]),
models.Index(fields=["ts", "thread_ts", "slack_channel_id"]),
]
@property
def slack_channel_slack_id(self) -> typing.Optional[str]:
return self.slack_channel.slack_id if self.slack_channel else None
def get_resolution_note(self) -> typing.Optional["ResolutionNote"]:
try:
return self.resolution_note
except ResolutionNoteSlackMessage.resolution_note.RelatedObjectDoesNotExist:
return None
def delete(self, *args, **kwargs) -> typing.Tuple[int, typing.Dict[str, int]]:
resolution_note = self.get_resolution_note()
if resolution_note:
resolution_note.delete()
return super().delete(*args, **kwargs)
class ResolutionNoteQueryset(models.QuerySet):
def delete(self):
self.update(deleted_at=timezone.now())
def hard_delete(self):
super().delete()
def filter(self, *args, **kwargs):
return super().filter(*args, **kwargs, deleted_at__isnull=True)
class ResolutionNote(models.Model):
alert_group: "AlertGroup"
author: typing.Optional["User"]
resolution_note_slack_message: typing.Optional[ResolutionNoteSlackMessage]
objects = ResolutionNoteQueryset.as_manager()
objects_with_deleted = models.Manager()
class Source(models.IntegerChoices):
SLACK = 0, "Slack"
WEB = 1, "Web"
MOBILE_APP = 2, "Mobile App"
public_primary_key = models.CharField(
max_length=20,
validators=[MinLengthValidator(settings.PUBLIC_PRIMARY_KEY_MIN_LENGTH + 1)],
unique=True,
default=generate_public_primary_key_for_resolution_note,
)
alert_group = models.ForeignKey(
"alerts.AlertGroup",
on_delete=models.CASCADE,
related_name="resolution_notes",
)
source = models.IntegerField(choices=Source.choices, default=None, null=True)
author = models.ForeignKey(
"user_management.User",
on_delete=models.SET_NULL,
null=True,
default=None,
related_name="authored_resolution_notes",
)
message_text = models.TextField(max_length=3000, default=None, null=True)
created_at = models.DateTimeField(auto_now_add=True)
resolution_note_slack_message = models.OneToOneField(
"alerts.ResolutionNoteSlackMessage",
on_delete=models.SET_NULL,
null=True,
default=None,
related_name="resolution_note",
)
deleted_at = models.DateTimeField(default=None, null=True)
def delete(self):
ResolutionNote.objects.filter(pk=self.pk).delete()
def hard_delete(self):
super().delete()
@property
def text(self):
if self.source == ResolutionNote.Source.SLACK:
return self.resolution_note_slack_message.text
return self.message_text
def recreate(self):
"""
Recreates soft-deleted resolution note.
E.g. resolution note can be removed and then added again in slack.
"""
self.deleted_at = None
self.save(update_fields=["deleted_at"])
def render_log_line_json(self):
time = humanize.naturaldelta(self.alert_group.started_at - self.created_at)
created_at = DateTimeField().to_representation(self.created_at)
organization = self.alert_group.channel.organization
author = self.author.short(organization) if self.author is not None else None
sf = SlackFormatter(organization)
action = sf.format(self.text)
action = clean_markup(action)
result = {
"time": time,
"action": action,
"realm": "resolution_note",
"type": self.source,
"created_at": created_at,
"author": author,
}
return result
def author_verbal(self, mention: bool) -> str:
"""
Postmortems to resolution notes included migrating `AlertGroupPostmortem` to `ResolutionNote`s.
But `AlertGroupPostmortem` has no author field. So this method was introduced as a workaround.
(see git history for more details on what `AlertGroupPostmortem` was)
"""
return "" if self.author is None else self.author.get_username_with_slack_verbal(mention)