# What this PR does Similar to https://github.com/grafana/oncall/pull/5199 Converts follow char fields to primary key relationships on `SlackChannel` table: - `ResolutionNoteSlackMessage.channel_id` -> `ResolutionNoteSlackMessage.slack_channel` - `ChannelFilter.slack_channel_id` -> `ChannelFilter.slack_channel` ## 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.
133 lines
4.5 KiB
Python
133 lines
4.5 KiB
Python
import datetime
|
|
import logging
|
|
import typing
|
|
from functools import partial
|
|
|
|
from django.db import models, transaction
|
|
|
|
from apps.alerts import tasks
|
|
|
|
if typing.TYPE_CHECKING:
|
|
from apps.alerts.models import AlertGroup
|
|
from apps.user_management.models import User
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
|
|
class Invitation(models.Model):
|
|
"""
|
|
It's an invitation of a user to join working on Alert Group
|
|
"""
|
|
|
|
alert_group: "AlertGroup"
|
|
author: typing.Optional["User"]
|
|
invitee: typing.Optional["User"]
|
|
|
|
ATTEMPTS_LIMIT = 10
|
|
|
|
time_deltas_by_attempts = [
|
|
datetime.timedelta(minutes=6),
|
|
datetime.timedelta(minutes=16),
|
|
datetime.timedelta(minutes=31),
|
|
datetime.timedelta(hours=1, minutes=1),
|
|
datetime.timedelta(hours=3, minutes=1),
|
|
]
|
|
|
|
author = models.ForeignKey(
|
|
"user_management.User",
|
|
null=True,
|
|
on_delete=models.SET_NULL,
|
|
related_name="author_of_invitations",
|
|
)
|
|
|
|
invitee = models.ForeignKey(
|
|
"user_management.User",
|
|
null=True,
|
|
on_delete=models.SET_NULL,
|
|
related_name="invitee_in_invitations",
|
|
)
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
is_active = models.BooleanField(default=True)
|
|
alert_group = models.ForeignKey("alerts.AlertGroup", on_delete=models.CASCADE, related_name="invitations")
|
|
attempt = models.IntegerField(default=0)
|
|
|
|
@property
|
|
def attempts_left(self) -> int:
|
|
return Invitation.ATTEMPTS_LIMIT - self.attempt
|
|
|
|
@staticmethod
|
|
def get_delay_by_attempt(attempt: int) -> datetime.timedelta:
|
|
countdown = Invitation.time_deltas_by_attempts[-1]
|
|
if attempt < len(Invitation.time_deltas_by_attempts):
|
|
countdown = Invitation.time_deltas_by_attempts[attempt]
|
|
return countdown
|
|
|
|
@staticmethod
|
|
def invite_user(invitee_user: "User", alert_group: "AlertGroup", user: "User") -> None:
|
|
from apps.alerts.models import AlertGroupLogRecord
|
|
|
|
# RFCT - why atomic? without select for update?
|
|
with transaction.atomic():
|
|
try:
|
|
invitation = Invitation.objects.get(
|
|
invitee=invitee_user,
|
|
alert_group=alert_group,
|
|
is_active=True,
|
|
)
|
|
invitation.is_active = False
|
|
invitation.save(update_fields=["is_active"])
|
|
log_record = AlertGroupLogRecord(
|
|
type=AlertGroupLogRecord.TYPE_RE_INVITE, author=user, alert_group=alert_group
|
|
)
|
|
except Invitation.DoesNotExist:
|
|
log_record = AlertGroupLogRecord(
|
|
type=AlertGroupLogRecord.TYPE_INVITE,
|
|
author=user,
|
|
alert_group=alert_group,
|
|
)
|
|
invitation = Invitation(
|
|
invitee=invitee_user,
|
|
alert_group=alert_group,
|
|
is_active=True,
|
|
author=user,
|
|
)
|
|
invitation.save()
|
|
|
|
log_record.invitation = invitation
|
|
log_record.save()
|
|
logger.debug(
|
|
f"call send_alert_group_signal for alert_group {alert_group.pk}, "
|
|
f"log record {log_record.pk} with type '{log_record.get_type_display()}'"
|
|
)
|
|
|
|
transaction.on_commit(partial(tasks.send_alert_group_signal.delay, log_record.pk))
|
|
transaction.on_commit(partial(tasks.invite_user_to_join_incident.delay, invitation.pk))
|
|
|
|
@staticmethod
|
|
def stop_invitation(invitation_pk: int, user: "User") -> None:
|
|
from apps.alerts.models import AlertGroupLogRecord
|
|
|
|
with transaction.atomic():
|
|
try:
|
|
invitation = Invitation.objects.filter(pk=invitation_pk).select_for_update()[0]
|
|
except IndexError:
|
|
return f"stop_invitation: Invitation with pk {invitation_pk} doesn't exist"
|
|
invitation.is_active = False
|
|
invitation.save(update_fields=["is_active"])
|
|
|
|
log_record = AlertGroupLogRecord(
|
|
type=AlertGroupLogRecord.TYPE_STOP_INVITATION,
|
|
author=user,
|
|
alert_group=invitation.alert_group,
|
|
invitation=invitation,
|
|
)
|
|
|
|
log_record.save()
|
|
logger.debug(
|
|
f"call send_alert_group_signal for alert_group {invitation.alert_group.pk}, "
|
|
f"log record {log_record.pk} with type '{log_record.get_type_display()}'"
|
|
)
|
|
transaction.on_commit(partial(tasks.send_alert_group_signal.delay, log_record.pk))
|