diff --git a/experimenter/experimenter/nimbus_ui/forms.py b/experimenter/experimenter/nimbus_ui/forms.py
index 4ae6baa7fc..2114139ea2 100644
--- a/experimenter/experimenter/nimbus_ui/forms.py
+++ b/experimenter/experimenter/nimbus_ui/forms.py
@@ -1479,33 +1479,31 @@ def save(self, commit=True):
class FeaturesForm(forms.ModelForm):
+ def get_feature_config_choices(self, application, qs):
+ choices = []
+ choices.extend(
+ sorted(
+ [
+ (feature.pk, f"{feature.name} - {feature.description}")
+ for feature in qs
+ ],
+ key=lambda choice: choice[1].lower(),
+ )
+ )
+ return choices
+
application = forms.ChoiceField(
- label="",
- choices=NimbusExperiment.Application.choices,
- widget=forms.widgets.Select(
- attrs={
- "class": "form-select",
- },
- ),
- initial=NimbusExperiment.Application.DESKTOP.value,
+ required=False,
+ choices=[("", "Nothing selected"), *list(NimbusExperiment.Application.choices)],
+ widget=SingleSelectWidget(),
)
feature_configs = forms.ChoiceField(
- label="",
- choices=[],
+ required=False,
+ choices=[("", "Nothing selected")],
widget=SingleSelectWidget(),
)
update_on_change_fields = ("application", "feature_configs")
- def get_feature_config_choices(self, application, qs):
- return sorted(
- [
- (application.pk, f"{application.name} - {application.description}")
- for application in NimbusFeatureConfig.objects.all()
- if application in qs
- ],
- key=lambda choice: choice[1].lower(),
- )
-
class Meta:
model = NimbusFeatureConfig
fields = ["application", "feature_configs"]
@@ -1513,15 +1511,18 @@ class Meta:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- selected_app = self.data.get("application") or self.get_initial_for_field(
- self.fields["application"], "application"
- )
- features = NimbusFeatureConfig.objects.filter(application=selected_app).order_by(
- "slug"
- )
- self.fields["feature_configs"].choices = self.get_feature_config_choices(
- selected_app, features
- )
+ # Default: nothing selected for application and features
+ selected_app = self.data.get("application") or self.initial.get("application")
+ if selected_app:
+ features = NimbusFeatureConfig.objects.filter(
+ application=selected_app
+ ).order_by("slug")
+ self.fields["feature_configs"].choices = self.get_feature_config_choices(
+ selected_app, features
+ )
+
+ self.fields["application"].initial = ""
+ self.fields["feature_configs"].initial = ""
base_url = reverse("nimbus-ui-features")
htmx_attrs = {
@@ -1531,6 +1532,7 @@ def __init__(self, *args, **kwargs):
"hx-select": "#features-form",
"hx-target": "#features-form",
"hx-swap": "outerHTML",
+ "hx-select-oob": "#deliveries-table",
}
self.fields["application"].widget.attrs.update(htmx_attrs)
self.fields["feature_configs"].widget.attrs.update(htmx_attrs)
diff --git a/experimenter/experimenter/nimbus_ui/templates/nimbus_experiments/features.html b/experimenter/experimenter/nimbus_ui/templates/nimbus_experiments/features.html
index d5fe9e03b6..4792fac0c0 100644
--- a/experimenter/experimenter/nimbus_ui/templates/nimbus_experiments/features.html
+++ b/experimenter/experimenter/nimbus_ui/templates/nimbus_experiments/features.html
@@ -17,24 +17,73 @@
hx-swap="outerHTML">
{% csrf_token %}
-
+
-
+
Application
{{ form.application|add_class:"form-control"|add_error_class:"is-invalid" }}
{% for error in form.application.errors %}
{{ error }}
{% endfor %}
-
+
Feature Config
{{ form.feature_configs|add_class:"form-control" }}
{% for error in form.feature_configs.errors %}
{{ error }}
{% endfor %}
-
{{ application }}
-
{{ feature_configs }}
+
+
+
+ Deliveries
+
+
+
+
+
+
+
+ | Recipe Name |
+ Launch Date |
+ Type Of Delivery |
+ Channel(s) |
+ Minimum Version |
+ Population Size |
+ Delivery Brief |
+
+
+
+ {% for experiment in experiments %}
+
+ |
+ {{ experiment.name }}
+ |
+ {{ experiment.published_date|format_not_set }} |
+ {{ experiment.home_type_choice }} |
+ {{ experiment.get_channel_display }} |
+ {{ experiment.firefox_min_version|parse_version }} |
+ {{ experiment.population_percent|floatformat:"-1" }} |
+ {{ experiment.delivery_brief|format_not_set }} |
+ {% empty %}
+
+
+ No Deliveries
+ |
+
+ {% endfor %}
+
+
+
+
+
+
{% endblock %}
diff --git a/experimenter/experimenter/nimbus_ui/tests/test_forms.py b/experimenter/experimenter/nimbus_ui/tests/test_forms.py
index 8b0fa7aefe..d53b2a1c5a 100644
--- a/experimenter/experimenter/nimbus_ui/tests/test_forms.py
+++ b/experimenter/experimenter/nimbus_ui/tests/test_forms.py
@@ -3835,8 +3835,8 @@ def test_features_view_default_fields_are_firefox_desktop(self):
form = FeaturesForm()
application = form.fields["application"]
feature_configs = form.fields["feature_configs"]
- self.assertEqual(application.initial, NimbusExperiment.Application.DESKTOP.value)
- self.assertIsNone(feature_configs.initial)
+ self.assertEqual(application.initial, "")
+ self.assertEqual(feature_configs.initial, "")
@parameterized.expand(
[
diff --git a/experimenter/experimenter/nimbus_ui/tests/test_views.py b/experimenter/experimenter/nimbus_ui/tests/test_views.py
index 14a14944cb..1e80b40a58 100644
--- a/experimenter/experimenter/nimbus_ui/tests/test_views.py
+++ b/experimenter/experimenter/nimbus_ui/tests/test_views.py
@@ -3366,8 +3366,9 @@ def setUp(self):
"feature-mobile": NimbusExperiment.Application.IOS,
"feature-web": NimbusExperiment.Application.EXPERIMENTER,
}
+ self.feature_configs = {}
for item, value in self.features.items():
- NimbusFeatureConfigFactory.create(
+ self.feature_configs[item] = NimbusFeatureConfigFactory.create(
slug=item, name=item.replace("-", " "), application=value
)
@@ -3383,11 +3384,9 @@ def test_features_view_dropdown_loads_correct_default(self):
form = response.context["form"]
self.assertTrue(form.fields["application"])
- self.assertEqual(
- form.fields["application"].initial, NimbusExperiment.Application.DESKTOP.value
- )
+ self.assertEqual(form.fields["application"].initial, "")
self.assertTrue(form.fields["feature_configs"])
- self.assertEqual(form.fields["feature_configs"].initial, None)
+ self.assertEqual(form.fields["feature_configs"].initial, "")
@parameterized.expand(
[
@@ -3434,3 +3433,30 @@ def test_features_view_multiapplication_loads_in_feature_config(self):
self.assertTrue(form.fields["application"])
self.assertEqual(form["application"].value(), applications[1].value)
self.assertEqual(form["feature_configs"].value(), str(feature_config_multi.id))
+
+ @parameterized.expand(
+ [
+ (NimbusExperiment.Application.DESKTOP, "feature-desktop"),
+ (NimbusExperiment.Application.IOS, "feature-mobile"),
+ (NimbusExperiment.Application.EXPERIMENTER, "feature-web"),
+ ]
+ )
+ def test_features_view_renders_table_with_correct_elements(
+ self, application, feature_config
+ ):
+ experiment = f"Experiment {feature_config.replace('-', ' ')}"
+ NimbusExperimentFactory.create(
+ name=experiment,
+ application=application,
+ feature_configs=[self.feature_configs[feature_config]],
+ )
+
+ feature_id = self.feature_configs[feature_config].id
+ url = reverse("nimbus-ui-features")
+ response = self.client.get(
+ f"{url}?application={application.value}&feature_configs={feature_id}"
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "#deliveries-table")
+ self.assertContains(response, experiment)
diff --git a/experimenter/experimenter/nimbus_ui/views.py b/experimenter/experimenter/nimbus_ui/views.py
index a91c15d016..fd52bad3e3 100644
--- a/experimenter/experimenter/nimbus_ui/views.py
+++ b/experimenter/experimenter/nimbus_ui/views.py
@@ -644,6 +644,25 @@ class NimbusFeaturesView(TemplateView):
def get_form(self):
return FeaturesForm(self.request.GET or None)
+ def get_queryset(self):
+ qs = (
+ NimbusExperiment.objects.with_merged_channel()
+ .filter(is_archived=False)
+ .order_by("-_updated_date_time")
+ )
+
+ app = self.request.GET.get("application")
+ if app:
+ qs = qs.filter(application=app)
+
+ feature_id = self.request.GET.get("feature_configs")
+ if feature_id:
+ qs = qs.filter(feature_configs=feature_id).distinct()
+ else:
+ return None
+
+ return qs
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
form = self.get_form()
@@ -651,6 +670,7 @@ def get_context_data(self, **kwargs):
context["links"] = NimbusUIConstants.FEATURE_PAGE_LINKS
context["application"] = self.request.GET.get("application")
context["feature_configs"] = self.request.GET.get("feature_configs")
+ context["experiments"] = self.get_queryset()
return context