Skip to content
Merged
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
33 changes: 33 additions & 0 deletions experimenter/experimenter/experiments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,39 @@ def end_date(self):
self.save()
return self._end_date

@property
def days_since_enrollment_start(self):
if self.enrollment_start_date is not None:
return (datetime.date.today() - self.enrollment_start_date).days

@property
def enrollment_percent_completion(self):
if self.days_since_enrollment_start is not None and self.computed_enrollment_days:
percent = (
self.days_since_enrollment_start / self.computed_enrollment_days
) * 100
return min(round(percent), 100.0)

@property
def days_since_observation_start(self):
if (
enrollment_end_date := (
self.actual_enrollment_end_date or self.computed_enrollment_end_date
)
) is not None:
return (datetime.date.today() - enrollment_end_date).days

@property
def observation_percent_completion(self):
if (
self.days_since_observation_start is not None
and self.computed_observations_days
):
percent = (
self.days_since_observation_start / self.computed_observations_days
) * 100
return min(round(percent), 100.0)

@property
def proposed_enrollment_end_date(self):
if (
Expand Down
63 changes: 46 additions & 17 deletions experimenter/experimenter/experiments/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1584,6 +1584,36 @@ def test_end_date_uses_cached_end_date(self):

self.assertEqual(experiment.end_date, cached_date)

def test_days_of_enrollment(self):
experiment = NimbusExperimentFactory.create_with_lifecycle(
NimbusExperimentFactory.Lifecycles.LIVE_ENROLLING,
start_date=datetime.date.today() - datetime.timedelta(days=2),
)
self.assertEqual(experiment.days_since_enrollment_start, 2)

def test_enrollment_completion_percent(self):
experiment = NimbusExperimentFactory.create_with_lifecycle(
NimbusExperimentFactory.Lifecycles.LIVE_ENROLLING,
_enrollment_end_date=datetime.date.today() + datetime.timedelta(days=8),
start_date=datetime.date.today() - datetime.timedelta(days=2),
)
self.assertEqual(experiment.enrollment_percent_completion, 20)

def test_days_of_observation(self):
experiment = NimbusExperimentFactory.create_with_lifecycle(
NimbusExperimentFactory.Lifecycles.LIVE_ENROLLING,
_enrollment_end_date=datetime.date.today() - datetime.timedelta(days=4),
)
self.assertEqual(experiment.days_since_observation_start, 4)

def test_observation_completion_percent(self):
experiment = NimbusExperimentFactory.create_with_lifecycle(
NimbusExperimentFactory.Lifecycles.LIVE_ENROLLING,
_computed_end_date=datetime.date.today() + datetime.timedelta(days=9),
_enrollment_end_date=datetime.date.today() - datetime.timedelta(days=1),
)
self.assertEqual(experiment.observation_percent_completion, 10)

def test_enrollment_duration_for_ended_experiment(self):
experiment = NimbusExperimentFactory.create_with_lifecycle(
NimbusExperimentFactory.Lifecycles.LIVE_ENROLLING,
Expand Down Expand Up @@ -2673,27 +2703,26 @@ def test_get_kpi_metrics_returns_correct_metrics(
self.assertListEqual(kpi_metrics, expected_kpi_metrics)

def test_get_defaults_metrics_with_exclusions(self):
experiment = NimbusExperimentFactory.create()

experiment.results_data = {
"v3": {
"metadata": {
"metrics": {
"metricA": {
"retained": "Retained",
experiment = NimbusExperimentFactory.create(
results_data={
"v3": {
"metadata": {
"metrics": {
"metricA": {
"retained": "Retained",
"search_count": "Search Count",
}
},
},
"other_metrics": {
"other_metrics": {
"retained": "2 Week Retention",
"search_count": "Search Count",
}
},
},
"other_metrics": {
"other_metrics": {
"retained": "2 Week Retention",
"search_count": "Search Count",
}
},
}
}
}
experiment.save()
)

remaining_metrics = experiment.get_remaining_metrics_metadata(
exclude_slugs=["search_count"]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<button type="button"
class="btn btn-secondary"
hx-post="{% url cancel_reject_url slug=experiment.slug %}?fragment={{ fragment }}"
hx-select="{{ replaced_element|default:"#content" }}"
hx-target="{{ replaced_element|default:"#content" }}"
hx-swap="outerHTML"
hx-vals='{"cancel_message": "cancelled the review request to {{ experiment.review_messages }}"}'>
Cancel Review
</button>
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,20 @@
<button type="button"
class="btn btn-success"
hx-post="{% url approval_url slug=experiment.slug %}"
hx-select="#content"
hx-target="#content"
hx-select="{{ replaced_element|default:"#content" }}"
hx-target="{{ replaced_element|default:"#content" }}"
hx-vals='{"fragment": "{{ fragment }}"}'
hx-swap="outerHTML">Approve and {{ action_label }}</button>
<button type="button" class="btn btn-danger" id="reject-button">Reject</button>
</div>
{% endif %}
{% endif %}
<div class="alert alert-primary">
<button type="button"
class="btn btn-secondary"
hx-post="{% url cancel_reject_url slug=experiment.slug %}"
hx-select="#content"
hx-target="#content"
hx-swap="outerHTML"
hx-vals='{"cancel_message": "cancelled the review request to {{ experiment.review_messages }}"}'>
Cancel Review
</button>
</div>
{% if not replaced_element %}
<div class="alert alert-primary">
{% include "common/cancel_review_button.html" %}

</div>
{% endif %}
</div>
<!-- Rejection Form -->
<div id="reject-form-container" class="d-none alert alert-warning">
Expand All @@ -69,8 +65,9 @@
id="submit-rejection-button"
class="btn btn-danger mt-2"
hx-post="{% url cancel_reject_url slug=experiment.slug %}"
hx-select="#content"
hx-target="#content"
hx-select="{{ replaced_element|default:"#content" }}"
hx-target="{{ replaced_element|default:"#content" }}"
hx-vals='{"fragment": "{{ fragment }}"}'
hx-swap="outerHTML"
hx-include="[name='changelog_message']">Submit Rejection</button>
<button type="button" class="btn btn-secondary mt-2" id="cancel">Cancel</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
{% load nimbus_extras %}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can use the launch controls to show the progress 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should perhaps have a launch_controls_v2 since the Figma design uses different wording and has additional buttons in some cases.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right and then we can just replace that once we have complete new ui


<div id="{{ experiment.slug }}-progress-card">
{% with rejection=experiment.rejection_block %}
{% if rejection %}
<div class="alert alert-warning"
id="rejection-reason"
data-testid="rejection-notice">
<div class="text-body">
<p class="mb-2">
The request to {{ rejection.action }} was <strong>Rejected</strong> due to:
</p>
<p class="mb-2">{{ rejection.email }} on {{ rejection.date|date:"F j, Y" }}:</p>
<p class="bg-light text-dark rounded border p-2 mb-0">{{ rejection.message }}</p>
</div>
</div>
{% endif %}
{% endwith %}
{% if experiment|should_show_remote_settings_pending:user %}
<div class="alert alert-danger" role="alert">
<p>
<strong>Action required:</strong>
Please review this change in Remote Settings to {{ experiment.remote_settings_pending_message }}.
</p>
<a href="{{ experiment.review_url }}"
class="btn btn-primary"
target="_blank"
rel="noopener noreferrer">Open Remote Settings</a>
</div>
{% else %}
<form>
{% csrf_token %}
{% if experiment.is_rollout_update_requested %}
{% include "nimbus_experiments/approval_rejection_controls.html" with action_label="Update Rollout" approval_url="nimbus-ui-approve-update-rollout" cancel_reject_url="nimbus-ui-cancel-update-rollout" experiment=experiment fragment="progress_card" replaced_element="#"|add:experiment.slug|add:"-progress-card" %}

{% elif experiment.is_enrollment_pause_requested %}
{% include "nimbus_experiments/approval_rejection_controls.html" with action_label="End Enrollment" approval_url="nimbus-ui-approve-end-enrollment" cancel_reject_url="nimbus-ui-cancel-end-enrollment" experiment=experiment fragment="progress_card" replaced_element="#"|add:experiment.slug|add:"-progress-card" %}

{% elif experiment.is_end_experiment_requested %}
{% if experiment.is_rollout %}
{% include "nimbus_experiments/approval_rejection_controls.html" with action_label="End Rollout" approval_url="nimbus-ui-approve-end-experiment" cancel_reject_url="nimbus-ui-cancel-end-experiment" experiment=experiment fragment="progress_card" replaced_element="#"|add:experiment.slug|add:"-progress-card" %}

{% else %}
{% include "nimbus_experiments/approval_rejection_controls.html" with action_label="End Experiment" approval_url="nimbus-ui-approve-end-experiment" cancel_reject_url="nimbus-ui-cancel-end-experiment" experiment=experiment fragment="progress_card" replaced_element="#"|add:experiment.slug|add:"-progress-card" %}

{% endif %}
{% endif %}
</form>
{% endif %}
{% if experiment.is_enrolling %}
<div class="card bg-info-subtle p-4 mb-0 rounded-4">
<div class="row align-items-stretch">
<div class="col-5 py-4 mt-0">
<h5>Enrollment in progress — don't forget to close it</h5>
<p class="text-muted mb-0">
Enrollment is when participants can join your experiment. You'll need to end it yourself once enough people have joined, or when the time you set for enrollment has passed — it won't close automatically.
</p>
{% if experiment.should_show_end_enrollment %}
<div class="mt-4">
<form>
{% csrf_token %}
{% if experiment.is_rollout_update_requested %}
{% include "common/cancel_review_button.html" with cancel_reject_url="nimbus-ui-cancel-update-rollout" experiment=experiment fragment="progress_card" replaced_element="#"|add:experiment.slug|add:"-progress-card" %}

{% elif experiment.is_enrollment_pause_requested %}
{% include "common/cancel_review_button.html" with cancel_reject_url="nimbus-ui-cancel-end-enrollment" experiment=experiment fragment="progress_card" replaced_element="#"|add:experiment.slug|add:"-progress-card" %}

{% elif experiment.is_end_experiment_requested %}
{% include "common/cancel_review_button.html" with cancel_reject_url="nimbus-ui-cancel-end-experiment" experiment=experiment fragment="progress_card" replaced_element="#"|add:experiment.slug|add:"-progress-card" %}

{% elif experiment.should_show_end_enrollment or experiment.should_show_end_experiment or experiment.should_show_end_rollout or experiment.should_show_rollout_request_update %}
{% if experiment.should_show_end_enrollment %}
<button type="button"
hx-post="{% url 'nimbus-ui-live-to-end-enrollment' slug=experiment.slug %}?fragment=progress_card"
hx-select="#{{ experiment.slug }}-progress-card"
hx-target="#{{ experiment.slug }}-progress-card"
hx-swap="outerHTML"
class="btn btn-primary end-enrollment_btn"
{% if experiment.is_rollout_dirty %}disabled{% endif %}>Close Enrollment</button>
{% endif %}
{% if experiment.should_show_end_experiment %}
<button type="button"
hx-post="{% url 'nimbus-ui-live-to-complete' slug=experiment.slug %}?fragment=progress_card"
hx-select="#{{ experiment.slug }}-progress-card"
hx-target="#{{ experiment.slug }}-progress-card"
hx-swap="outerHTML"
id="end-experiment"
class="btn btn-primary m-1 end_experiment_btn">
End
{% if experiment.is_rollout %}
Rollout
{% else %}
Experiment
{% endif %}
</button>
{% endif %}
{% endif %}
</form>
</div>
{% endif %}
</div>
<div class="col">
<div class="card p-2 bg-body-secondary bg-opacity-50 py-5 px-4 rounded-4 h-100 d-flex justify-content-center">
<h6>{{ experiment.enrollment_percent_completion }}% of enrollment complete</h6>
<div class="progress mb-2 bg-secondary-subtle"
role="progressbar"
aria-label="{{ experiment.name }} enrollment progress"
aria-valuenow="{{ experiment.enrollment_percent_completion }}"
aria-valuemin="0"
aria-valuemax="100"
style="height: 10px">
<div class="progress-bar"
style="width: {{ experiment.enrollment_percent_completion }}%"></div>
</div>
<div class="text-muted d-flex gap-2 align-items-center mb-1">
<i class="fa-regular fa-calendar"></i>
<small>{{ experiment.days_since_enrollment_start }}/{{ experiment.proposed_enrollment }} days before end of enrollment</small>
</div>
<div class="text-muted d-flex gap-2 align-items-center">
<i class="fa-regular fa-circle-user"></i>
<small><a href="{{ experiment.monitoring_dashboard_url }}" class="text-reset">Check participant enrollment count</a></small>
</div>
</div>
</div>
</div>
</div>
{% elif experiment.is_observation %}
<div class="card bg-primary-subtle p-4 mb-0 rounded-4">
<div class="row align-items-center">
<div class="col-5 py-4 mt-0">
<h5>Observing — results still forming</h5>
<p class="text-muted mb-0">
Your experiment is officially in progress, and early numbers are starting to come in. These results are just for checking that everything's running correctly — they aren't reliable for decisions until the monitoring period is complete. <a href="https://experimenter.info/observing">
Learn more
</p>
</a>
<div class="mt-4">
<form>
{% csrf_token %}
{% if experiment.is_rollout_update_requested %}
{% include "common/cancel_review_button.html" with cancel_reject_url="nimbus-ui-cancel-update-rollout" experiment=experiment fragment="progress_card" replaced_element="#"|add:experiment.slug|add:"-progress-card" %}

{% elif experiment.is_enrollment_pause_requested %}
{% include "common/cancel_review_button.html" with cancel_reject_url="nimbus-ui-cancel-end-enrollment" experiment=experiment fragment="progress_card" replaced_element="#"|add:experiment.slug|add:"-progress-card" %}

{% elif experiment.is_end_experiment_requested %}
{% include "common/cancel_review_button.html" with cancel_reject_url="nimbus-ui-cancel-end-experiment" experiment=experiment fragment="progress_card" replaced_element="#"|add:experiment.slug|add:"-progress-card" %}

{% elif experiment.should_show_end_enrollment or experiment.should_show_end_experiment or experiment.should_show_end_rollout or experiment.should_show_rollout_request_update %}
{% if experiment.should_show_end_experiment %}
<button type="button"
hx-post="{% url 'nimbus-ui-live-to-complete' slug=experiment.slug %}?fragment=progress_card"
hx-select="#{{ experiment.slug }}-progress-card"
hx-target="#{{ experiment.slug }}-progress-card"
hx-swap="outerHTML"
id="end-experiment"
class="btn btn-primary m-1 end_experiment_btn">
End
{% if experiment.is_rollout %}
rollout
{% else %}
experiment
{% endif %}
early
</button>
{% endif %}
{% endif %}
</form>
</div>
</div>
<div class="col">
<div class="card p-2 bg-body-secondary bg-opacity-50 py-5 px-4 rounded-4">
<h6>{{ experiment.days_since_observation_start }}/{{ experiment.computed_observations_days }} days monitored</h6>
<div class="progress mb-2 bg-secondary-subtle"
role="progressbar"
aria-label="{{ experiment.name }} enrollment progress"
aria-valuenow="{{ experiment.observation_percent_completion }}"
aria-valuemin="0"
aria-valuemax="100"
style="height: 10px">
<div class="progress-bar"
style="width: {{ experiment.observation_percent_completion }}%"></div>
</div>
<div class="text-muted d-flex gap-2 align-items-center mb-1">
<i class="fa-solid fa-chart-line"></i>
<small>Results become reliable after {{ experiment.computed_observations_days }} days</small>
</div>
<div class="text-muted d-flex gap-2 align-items-center">
{% comment %} TODO(gh-13723): check if there are any metric errors and show them here {% endcomment %}
<i class="fa-solid fa-circle-check text-success"></i>
<small>Metrics reporting correctly</small>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
</select>
</div>
</form>
{% include "nimbus_experiments/launch_controls_v2.html" %}
{% include "nimbus_experiments/results-new-inner.html" %}

</div>
Loading