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

134 lines
4.5 KiB
Python
Raw Permalink Normal View History

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:
`apps.get_model` -> `import` (#2619) # What this PR does Remove [`apps.get_model`](https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.apps.get_model) invocations and use inline `import` statements in places where models are imported within functions/methods to avoid circular imports. I believe `import` statements are more appropriate for most use cases as they allow for better static code analysis & formatting, and solve the issue of circular imports without being unnecessarily dynamic as `apps.get_model`. With `import` statements, it's possible to: - Jump to model definitions in most IDEs - Automatically sort inline imports with `isort` - Find import errors faster/easier (most IDEs highlight broken imports) - Have more consistency across regular & inline imports when importing models This PR also adds a flake8 rule to ban imports of `django.apps.apps`, so it's harder to use `apps.get_model` by mistake (it's possible to ignore this rule by using `# noqa: I251`). The rule is not enforced on directories with migration files, because `apps.get_model` is often used to get a historical state of a model, which is useful when writing migrations ([see this SO answer for more details](https://stackoverflow.com/a/37769213)). So `apps.get_model` is considered OK in migrations (even necessary in some cases). ## 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] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
2023-07-25 10:43:23 +01:00
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:
`apps.get_model` -> `import` (#2619) # What this PR does Remove [`apps.get_model`](https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.apps.get_model) invocations and use inline `import` statements in places where models are imported within functions/methods to avoid circular imports. I believe `import` statements are more appropriate for most use cases as they allow for better static code analysis & formatting, and solve the issue of circular imports without being unnecessarily dynamic as `apps.get_model`. With `import` statements, it's possible to: - Jump to model definitions in most IDEs - Automatically sort inline imports with `isort` - Find import errors faster/easier (most IDEs highlight broken imports) - Have more consistency across regular & inline imports when importing models This PR also adds a flake8 rule to ban imports of `django.apps.apps`, so it's harder to use `apps.get_model` by mistake (it's possible to ignore this rule by using `# noqa: I251`). The rule is not enforced on directories with migration files, because `apps.get_model` is often used to get a historical state of a model, which is useful when writing migrations ([see this SO answer for more details](https://stackoverflow.com/a/37769213)). So `apps.get_model` is considered OK in migrations (even necessary in some cases). ## 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] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
2023-07-25 10:43:23 +01:00
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))