# What this PR does Reduces number of calls to db for `/schedules`, `/alertgroups` and `/users` endpoints. Fixes the issue when there was an additional call to db to get organization url to build user avatar full link. ## Which issue(s) this PR closes Related to [issue link here] <!-- *Note*: If you want the issue to be auto-closed once the PR is merged, change "Related to" to "Closes" in the line above. If you have more than one GitHub issue that this PR closes, be sure to preface each issue link with a [closing keyword](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests#linking-a-pull-request-to-an-issue). This ensures that the issue(s) are auto-closed once the PR has been merged. --> ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes.
225 lines
7.5 KiB
Python
225 lines
7.5 KiB
Python
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
|
|
|
|
|
|
def generate_public_primary_key_for_alert_group_postmortem():
|
|
prefix = "P"
|
|
new_public_primary_key = generate_public_primary_key(prefix)
|
|
|
|
failure_counter = 0
|
|
while AlertGroupPostmortem.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="AlertGroupPostmortem"
|
|
)
|
|
failure_counter += 1
|
|
|
|
return new_public_primary_key
|
|
|
|
|
|
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"]
|
|
|
|
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_id = models.CharField(max_length=100, null=True, default=None)
|
|
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"]),
|
|
]
|
|
|
|
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"
|
|
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):
|
|
"""
|
|
Postmortems to resolution notes included migrating AlertGroupPostmortem to ResolutionNotes.
|
|
But AlertGroupPostmortem has no author field. So this method was introduces as workaround.
|
|
"""
|
|
if self.author is not None:
|
|
return self.author.get_username_with_slack_verbal(mention)
|
|
else:
|
|
return ""
|
|
|
|
|
|
class AlertGroupPostmortem(models.Model):
|
|
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_alert_group_postmortem,
|
|
)
|
|
alert_group = models.ForeignKey(
|
|
"alerts.AlertGroup",
|
|
on_delete=models.CASCADE,
|
|
related_name="postmortem_text",
|
|
)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
last_modified = models.DateTimeField(auto_now=True)
|
|
text = models.TextField(max_length=3000, default=None, null=True)
|