Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions docs/experimenter/openapi-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@
"Draft",
"Preview",
"Live",
"Complete"
"Complete",
"Disabled"
]
}
},
Expand Down Expand Up @@ -273,7 +274,8 @@
"Draft",
"Preview",
"Live",
"Complete"
"Complete",
"Disabled"
]
}
},
Expand Down Expand Up @@ -621,7 +623,8 @@
"Draft",
"Preview",
"Live",
"Complete"
"Complete",
"Disabled"
]
}
},
Expand Down Expand Up @@ -699,7 +702,8 @@
"Draft",
"Preview",
"Live",
"Complete"
"Complete",
"Disabled"
]
}
},
Expand Down Expand Up @@ -783,7 +787,8 @@
"Draft",
"Preview",
"Live",
"Complete"
"Complete",
"Disabled"
]
}
},
Expand Down Expand Up @@ -888,7 +893,8 @@
"Draft",
"Preview",
"Live",
"Complete"
"Complete",
"Disabled"
]
}
},
Expand Down Expand Up @@ -1287,7 +1293,8 @@
"Draft",
"Preview",
"Live",
"Complete"
"Complete",
"Disabled"
],
"type": "string"
},
Expand Down
21 changes: 14 additions & 7 deletions docs/experimenter/swagger-ui.html
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@
"Draft",
"Preview",
"Live",
"Complete"
"Complete",
"Disabled"
]
}
},
Expand Down Expand Up @@ -285,7 +286,8 @@
"Draft",
"Preview",
"Live",
"Complete"
"Complete",
"Disabled"
]
}
},
Expand Down Expand Up @@ -633,7 +635,8 @@
"Draft",
"Preview",
"Live",
"Complete"
"Complete",
"Disabled"
]
}
},
Expand Down Expand Up @@ -711,7 +714,8 @@
"Draft",
"Preview",
"Live",
"Complete"
"Complete",
"Disabled"
]
}
},
Expand Down Expand Up @@ -795,7 +799,8 @@
"Draft",
"Preview",
"Live",
"Complete"
"Complete",
"Disabled"
]
}
},
Expand Down Expand Up @@ -900,7 +905,8 @@
"Draft",
"Preview",
"Live",
"Complete"
"Complete",
"Disabled"
]
}
},
Expand Down Expand Up @@ -1299,7 +1305,8 @@
"Draft",
"Preview",
"Live",
"Complete"
"Complete",
"Disabled"
],
"type": "string"
},
Expand Down
4 changes: 4 additions & 0 deletions experimenter/experimenter/experiments/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,10 @@ class Status(models.TextChoices):
LIVE = "Live"
COMPLETE = "Complete"

# This status applies only to rollouts, and indicates that the rollout has been
# disabled. It is not a valid status for experiments.
DISABLED = "Disabled"

class PublishStatus(models.TextChoices):
IDLE = "Idle"
REVIEW = "Review"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Generated by Django 5.2.15 on 2026-07-15 18:14

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('experiments', '0333_nimbusrolloutplantemplate_and_more'),
]

operations = [
migrations.AlterField(
model_name='nimbuschangelog',
name='new_status',
field=models.CharField(choices=[('Draft', 'Draft'), ('Preview', 'Preview'), ('Live', 'Live'), ('Complete', 'Complete'), ('Disabled', 'Disabled')], max_length=255),
),
migrations.AlterField(
model_name='nimbuschangelog',
name='new_status_next',
field=models.CharField(blank=True, choices=[('Draft', 'Draft'), ('Preview', 'Preview'), ('Live', 'Live'), ('Complete', 'Complete'), ('Disabled', 'Disabled')], max_length=255, null=True),
),
migrations.AlterField(
model_name='nimbuschangelog',
name='old_status',
field=models.CharField(blank=True, choices=[('Draft', 'Draft'), ('Preview', 'Preview'), ('Live', 'Live'), ('Complete', 'Complete'), ('Disabled', 'Disabled')], max_length=255, null=True),
),
migrations.AlterField(
model_name='nimbuschangelog',
name='old_status_next',
field=models.CharField(blank=True, choices=[('Draft', 'Draft'), ('Preview', 'Preview'), ('Live', 'Live'), ('Complete', 'Complete'), ('Disabled', 'Disabled')], max_length=255, null=True),
),
migrations.AlterField(
model_name='nimbusexperiment',
name='status',
field=models.CharField(choices=[('Draft', 'Draft'), ('Preview', 'Preview'), ('Live', 'Live'), ('Complete', 'Complete'), ('Disabled', 'Disabled')], default='Draft', max_length=255, verbose_name='Status'),
),
migrations.AlterField(
model_name='nimbusexperiment',
name='status_next',
field=models.CharField(blank=True, choices=[('Draft', 'Draft'), ('Preview', 'Preview'), ('Live', 'Live'), ('Complete', 'Complete'), ('Disabled', 'Disabled')], max_length=255, null=True),
),
]
98 changes: 88 additions & 10 deletions experimenter/experimenter/experiments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,13 +545,27 @@ class Meta:

class Filters:
IS_LAUNCH_QUEUED = Q(
status=NimbusConstants.Status.DRAFT,
status_next=NimbusConstants.Status.LIVE,
Q(
status=NimbusConstants.Status.DRAFT,
status_next=NimbusConstants.Status.LIVE,
)
| Q(
is_rollout=True,
status=NimbusConstants.Status.DISABLED,
status_next=NimbusConstants.Status.LIVE,
),
publish_status=NimbusConstants.PublishStatus.APPROVED,
)
IS_LAUNCHING = Q(
status=NimbusConstants.Status.DRAFT,
status_next=NimbusConstants.Status.LIVE,
Q(
status=NimbusConstants.Status.DRAFT,
status_next=NimbusConstants.Status.LIVE,
)
| Q(
is_rollout=True,
status=NimbusConstants.Status.DISABLED,
status_next=NimbusConstants.Status.LIVE,
),
publish_status=NimbusConstants.PublishStatus.WAITING,
)
IS_UPDATE_QUEUED = Q(
Expand All @@ -565,13 +579,27 @@ class Filters:
publish_status=NimbusConstants.PublishStatus.WAITING,
)
IS_END_QUEUED = Q(
status=NimbusConstants.Status.LIVE,
status_next=NimbusConstants.Status.COMPLETE,
Q(
status=NimbusConstants.Status.LIVE,
status_next=NimbusConstants.Status.COMPLETE,
)
| Q(
is_rollout=True,
status=NimbusConstants.Status.LIVE,
status_next=NimbusConstants.Status.DISABLED,
),
publish_status=NimbusConstants.PublishStatus.APPROVED,
)
IS_ENDING = Q(
status=NimbusConstants.Status.LIVE,
status_next=NimbusConstants.Status.COMPLETE,
Q(
status=NimbusConstants.Status.LIVE,
status_next=NimbusConstants.Status.COMPLETE,
)
| Q(
is_rollout=True,
status=NimbusConstants.Status.LIVE,
status_next=NimbusConstants.Status.DISABLED,
),
publish_status=NimbusConstants.PublishStatus.WAITING,
)
SHOULD_ALLOCATE_BUCKETS = Q(
Expand Down Expand Up @@ -1285,7 +1313,10 @@ def advance_rollout_phase(self):
today = timezone.now().date()
phase_ids = [phase.id for phase in phases]

if self.rollout_phase_id is None:
if self.rollout_phase_next_id is not None:
current_phase = self.rollout_phase
next_phase = self.rollout_phase_next
elif self.rollout_phase_id is None:
current_phase = None
next_phase = phases[0]
else:
Expand All @@ -1297,7 +1328,9 @@ def advance_rollout_phase(self):
if next_phase is not None and not next_phase.population_percent:
return

if current_phase is not None:
if current_phase is not None and (
current_phase.end_date is None or current_phase.end_date > today
):
current_phase.end_date = today
if current_phase.actual_start_date:
current_phase.start_date = current_phase.actual_start_date
Expand All @@ -1312,8 +1345,52 @@ def advance_rollout_phase(self):
next_phase.save()
self.rollout_phase = next_phase
self.rollout_phase_next = None
self.population_percent = next_phase.population_percent
self.save()

def stage_rollout_phase_advance(self, copy_current_if_missing=False):
phases = list(self.rollout_phases.all())
if not phases:
return None

current_phase = self.rollout_phase
if self.rollout_phase_next_id is not None:
next_phase = self.rollout_phase_next
elif self.rollout_phase_id is None:
next_phase = phases[0]
else:
phase_ids = [phase.id for phase in phases]
current_index = phase_ids.index(self.rollout_phase_id)
next_index = current_index + 1
next_phase = phases[next_index] if next_index < len(phases) else None

if next_phase is None and copy_current_if_missing and current_phase is not None:
next_phase = self.rollout_phases.create(
population_percent=current_phase.population_percent
)

if next_phase is None or not next_phase.population_percent:
return None

self.rollout_phase_next = next_phase
self.population_percent = next_phase.population_percent
self.save(update_fields=["rollout_phase_next", "population_percent"])
return next_phase

def end_current_rollout_phase(self):
current_phase = self.rollout_phase
if current_phase is None:
return

today = timezone.now().date()
if current_phase.end_date is not None and current_phase.end_date <= today:
return

current_phase.end_date = today
if current_phase.actual_start_date:
current_phase.start_date = current_phase.actual_start_date
current_phase.save()

@property
def is_missing_takeaway_info(self):
return (
Expand Down Expand Up @@ -3457,6 +3534,7 @@ class Messages:
REJECTED_FROM_KINTO = "Rejected from Remote Settings"
LIVE = "Experiment is live"
COMPLETED = "Experiment is complete"
DISABLED = "Rollout is disabled"
RESULTS_UPDATED = "Experiment results updated"
MONITORING_DATA_UPDATED = "Experiment monitoring data updated"
HOLDBACK_ENROLLMENT_UPDATED = "Holdback enrollment period updated"
Expand Down
Loading
Loading