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
60 changes: 31 additions & 29 deletions experimenter/experimenter/nimbus_ui/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -1479,49 +1479,50 @@ 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"]

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 = {
Expand All @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,73 @@
hx-swap="outerHTML">
{% csrf_token %}
<div class="card mb-3">
<div class="card-body">
<div class="card-body bg-primary-subtle">
<div class="row mb-4">
<div class="col-6">
<label for="id_application" class="form-label">Application</label>
<h5 class="fw-semibold">Application</h5>
{{ form.application|add_class:"form-control"|add_error_class:"is-invalid" }}
{% for error in form.application.errors %}<div class="invalid-feedback">{{ error }}</div>{% endfor %}
</div>
<div class="col-6">
<label for="id_feature_config" class="form-label">Feature Config</label>
<h5 class="fw-semibold">Feature Config</h5>
{{ form.feature_configs|add_class:"form-control" }}
{% for error in form.feature_configs.errors %}<div class="invalid-feedback">{{ error }}</div>{% endfor %}
</div>
</div>
<div>{{ application }}</div>
<div>{{ feature_configs }}</div>
</div>
</div>
</form>
<div class="mt-5 card shadow-sm border-0 px-3 py-4 h-100 rounded-3 bg-primary-subtle">
<h5 class="fw-semibold">
<img src="{% static 'assets/my-deliveries.svg' %}"
alt="Hugging Foxes"
style="width: 60px;
height: auto" />
Deliveries
</h5>
<div>
<div>
<div id="no-deliveries"></div>
<table id="deliveries-table"
class="table table-hover table-borderless align-middle mb-0">
<thead>
<tr>
<th>Recipe Name</th>
<th>Launch Date</th>
<th>Type Of Delivery</th>
<th>Channel(s)</th>
<th>Minimum Version</th>
<th>Population Size</th>
<th>Delivery Brief</th>
</tr>
</thead>
<tbody>
{% for experiment in experiments %}
<tr scope="row">
<td id="delivery-name">
<a target="_blank"
href="{% url 'nimbus-ui-detail' slug=experiment.slug %}"
class="text-decoration-none fw-medium">{{ experiment.name }}</a>
</td>
<td id="delivery-published-date">{{ experiment.published_date|format_not_set }}</td>
<td id="delivery-type-{{ experiment.home_type_choice }}">{{ experiment.home_type_choice }}</td>
<td id="delivery-channel">{{ experiment.get_channel_display }}</td>
<td id="delivery-min-version">{{ experiment.firefox_min_version|parse_version }}</td>
<td id="delivery-population-percent">{{ experiment.population_percent|floatformat:"-1" }}</td>
<td id="delivery-brief">{{ experiment.delivery_brief|format_not_set }}</td>
{% empty %}
<tr>
<td colspan="12" class="fw-semibold text-center p-4">
<h4>No Deliveries</h4>
</td>
</tr>
{% endfor %}
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
4 changes: 2 additions & 2 deletions experimenter/experimenter/nimbus_ui/tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
[
Expand Down
36 changes: 31 additions & 5 deletions experimenter/experimenter/nimbus_ui/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand All @@ -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(
[
Expand Down Expand Up @@ -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)
20 changes: 20 additions & 0 deletions experimenter/experimenter/nimbus_ui/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,13 +644,33 @@ 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()
context["form"] = form
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


Expand Down