# 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)
122 lines
4.1 KiB
Python
122 lines
4.1 KiB
Python
import datetime
|
|
import logging
|
|
|
|
from django.db import models, transaction
|
|
|
|
from apps.alerts.tasks import invite_user_to_join_incident, send_alert_group_signal
|
|
|
|
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
|
|
"""
|
|
|
|
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):
|
|
return Invitation.ATTEMPTS_LIMIT - self.attempt
|
|
|
|
@staticmethod
|
|
def get_delay_by_attempt(attempt):
|
|
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, alert_group, user):
|
|
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()}'"
|
|
)
|
|
send_alert_group_signal.apply_async((log_record.pk,))
|
|
|
|
invite_user_to_join_incident.apply_async((invitation.pk,))
|
|
|
|
@staticmethod
|
|
def stop_invitation(invitation_pk, user):
|
|
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()}'"
|
|
)
|
|
send_alert_group_signal.apply_async((log_record.pk,))
|