|
@@ -51,10 +51,30 @@ class Relocation(DefaultFieldsModel):
|
|
|
def get_choices(cls) -> list[tuple[int, str]]:
|
|
|
return [(key.value, key.name) for key in cls]
|
|
|
|
|
|
+ # Like `get_choices` above, except it excludes the final `COMPLETED` step.
|
|
|
+ @classmethod
|
|
|
+ def get_in_progress_choices(cls) -> list[tuple[int, str]]:
|
|
|
+ return [(key.value, key.name) for key in cls if key.name != "COMPLETED"]
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def max_value(cls):
|
|
|
+ return max(item.value for item in cls)
|
|
|
+
|
|
|
class Status(Enum):
|
|
|
IN_PROGRESS = 0
|
|
|
FAILURE = 1
|
|
|
SUCCESS = 2
|
|
|
+ PAUSE = 3
|
|
|
+
|
|
|
+ # TODO(getsentry/team-ospo#190): Could we dedup this with a mixin in the future?
|
|
|
+ @classmethod
|
|
|
+ def get_choices(cls) -> list[tuple[int, str]]:
|
|
|
+ return [(key.value, key.name) for key in cls]
|
|
|
+
|
|
|
+ class EmailKind(Enum):
|
|
|
+ STARTED = 0
|
|
|
+ FAILED = 1
|
|
|
+ SUCCEEDED = 2
|
|
|
|
|
|
# TODO(getsentry/team-ospo#190): Could we dedup this with a mixin in the future?
|
|
|
@classmethod
|
|
@@ -84,6 +104,18 @@ class Relocation(DefaultFieldsModel):
|
|
|
choices=Status.get_choices(), default=Status.IN_PROGRESS.value
|
|
|
)
|
|
|
|
|
|
+ # Schedules a pause prior to some step that has not yet occurred. Useful to perform an orderly
|
|
|
+ # halting of the relocation. When unpausing, the unpausing process is responsible for scheduling
|
|
|
+ # the correct celery task so that the relocation may continue.
|
|
|
+ scheduled_pause_at_step = models.SmallIntegerField(
|
|
|
+ choices=Step.get_in_progress_choices(), null=True, default=None
|
|
|
+ )
|
|
|
+
|
|
|
+ # Schedules the termination of this relocation prior to some step that has not yet occurred.
|
|
|
+ scheduled_cancel_at_step = models.SmallIntegerField(
|
|
|
+ choices=Step.get_in_progress_choices(), null=True, default=None
|
|
|
+ )
|
|
|
+
|
|
|
# Organizations, identified by slug, which this relocation seeks to import, specified by the
|
|
|
# user as part of their relocation request. These slugs are relative to the import file, not
|
|
|
# their final value post-import, which may be changed to avoid collisions.
|
|
@@ -96,7 +128,12 @@ class Relocation(DefaultFieldsModel):
|
|
|
|
|
|
# The last status for which we have notified the user. It is `None` by default, to indicate that
|
|
|
# we have not yet sent the user a "your relocation is in progress" email.
|
|
|
- latest_notified = models.SmallIntegerField(choices=Step.get_choices(), null=True, default=None)
|
|
|
+ latest_notified = models.SmallIntegerField(
|
|
|
+ choices=EmailKind.get_choices(), null=True, default=None
|
|
|
+ )
|
|
|
+
|
|
|
+ # The last time we've sent the "claim your account" email blast to all unclaimed users.
|
|
|
+ latest_unclaimed_emails_sent_at = models.DateTimeField(null=True, default=None)
|
|
|
|
|
|
# The last task started by this relocation. Because tasks for a given relocation are always
|
|
|
# attempted sequentially, and never in concurrently (that is, there is always at most one task
|
|
@@ -114,6 +151,18 @@ class Relocation(DefaultFieldsModel):
|
|
|
class Meta:
|
|
|
app_label = "sentry"
|
|
|
db_table = "sentry_relocation"
|
|
|
+ constraints = [
|
|
|
+ models.CheckConstraint(
|
|
|
+ name="scheduled_pause_at_step_greater_than_current_step",
|
|
|
+ check=models.Q(scheduled_pause_at_step__gt=models.F("step"))
|
|
|
+ | models.Q(scheduled_pause_at_step__isnull=True),
|
|
|
+ ),
|
|
|
+ models.CheckConstraint(
|
|
|
+ name="scheduled_cancel_at_step_greater_than_current_step",
|
|
|
+ check=models.Q(scheduled_cancel_at_step__gt=models.F("step"))
|
|
|
+ | models.Q(scheduled_cancel_at_step__isnull=True),
|
|
|
+ ),
|
|
|
+ ]
|
|
|
|
|
|
|
|
|
@region_silo_only_model
|