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
29 changes: 29 additions & 0 deletions experimenter/experimenter/nimbus_ui/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -1651,3 +1651,32 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["tags"].queryset = Tag.objects.all().order_by("name")
self.fields["tags"].widget = forms.CheckboxSelectMultiple()


class CollaboratorsForm(NimbusChangeLogFormMixin, forms.ModelForm):
collaborators = forms.ModelMultipleChoiceField(
queryset=User.objects.all().order_by("email"),
widget=MultiSelectWidget(),
required=False,
label="Collaborators",
)

class Meta:
model = NimbusExperiment
fields = []

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Initialize the collaborators field with current subscribers
if self.instance and self.instance.pk:
self.fields["collaborators"].initial = self.instance.subscribers.all()

def save(self, commit=True):
experiment = super().save(commit=commit)
if commit:
# Update subscribers with selected collaborators
experiment.subscribers.set(self.cleaned_data["collaborators"])
return experiment

def get_changelog_message(self):
return f"{self.request.user} updated collaborators"
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<tr id="collaborators-row"
class="collaborators-section align-middle"
{% if oob %}hx-swap-oob="true"{% endif %}>
<th scope="row" class="text-nowrap">Collaborators</th>
<td colspan="4">
<form method="post"
action="{% url 'nimbus-ui-update-collaborators' slug=experiment.slug %}"
hx-post="{% url 'nimbus-ui-update-collaborators' slug=experiment.slug %}"
hx-trigger="change"
hx-target="#collaborators-subscribers-section"
hx-swap="outerHTML"
class="d-flex flex-column gap-2">
{% csrf_token %}
<div class="input-group">{{ collaborators_form.subscribers }}</div>
</form>
</td>
</tr>
Original file line number Diff line number Diff line change
@@ -1,35 +1,39 @@
<tr class="subscribers-section">
<th>Subscribers</th>
<td id="subscribers-list"
colspan="2"
style="max-height: 150px;
overflow-y: auto;
overflow-x: hidden">
{% for subscriber in experiment.subscribers.all %}
<p>{{ subscriber.email }}</p>
{% empty %}
<span class="text-danger">Not Set</span>
{% endfor %}
</td>
<td style="text-align: right;">
{% if request.user in experiment.subscribers.all %}
<form method="post"
action="{% url 'nimbus-ui-unsubscribe' slug=experiment.slug %}"
hx-post="{% url 'nimbus-ui-unsubscribe' slug=experiment.slug %}"
hx-target=".subscribers-section"
hx-swap="outerHTML">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Unsubscribe</button>
</form>
{% else %}
<form method="post"
action="{% url 'nimbus-ui-subscribe' slug=experiment.slug %}"
hx-post="{% url 'nimbus-ui-subscribe' slug=experiment.slug %}"
hx-target=".subscribers-section"
hx-swap="outerHTML">
{% csrf_token %}
<button type="submit" class="btn btn-success">Subscribe</button>
</form>
{% endif %}
</td>
</tr>
<tbody id="collaborators-subscribers-section"
hx-on="htmx:afterOnLoad: window.$ && $('#collaborators-subscribers-section .selectpicker').selectpicker('refresh')">
<tr id="subscribers-row" class="subscribers-section">
<th>Subscribers</th>
<td id="subscribers-list"
colspan="2"
style="max-height: 150px;
overflow-y: auto;
overflow-x: hidden">
{% for subscriber in experiment.subscribers.all %}
<p>{{ subscriber.email }}</p>
{% empty %} <span class="text-danger">Not Set</span>
{% endfor %}
</td>
<td style="text-align: right">
{% if request.user in experiment.subscribers.all %}
<form method="post"
action="{% url 'nimbus-ui-unsubscribe' slug=experiment.slug %}"
hx-post="{% url 'nimbus-ui-unsubscribe' slug=experiment.slug %}"
hx-target="#collaborators-subscribers-section"
hx-swap="outerHTML">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Unsubscribe</button>
</form>
{% else %}
<form method="post"
action="{% url 'nimbus-ui-subscribe' slug=experiment.slug %}"
hx-post="{% url 'nimbus-ui-subscribe' slug=experiment.slug %}"
hx-target="#collaborators-subscribers-section"
hx-swap="outerHTML">
{% csrf_token %}
<button type="submit" class="btn btn-success">Subscribe</button>
</form>
{% endif %}
</td>
</tr>
{% include "nimbus_experiments/collaborators_section.html" %}

</tbody>
44 changes: 44 additions & 0 deletions experimenter/experimenter/nimbus_ui/tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
CancelEndEnrollmentForm,
CancelEndExperimentForm,
CancelUpdateRolloutForm,
CollaboratorsForm,
DocumentationLinkCreateForm,
DocumentationLinkDeleteForm,
DraftToPreviewForm,
Expand Down Expand Up @@ -4093,6 +4094,49 @@ def test_create_tag_generates_unique_name(self):
self.assertEqual(len(tag.color), 7)


class TestCollaboratorsForm(RequestFormTestCase):
def test_collaborators_form_updates_subscribers(self):
experiment = NimbusExperimentFactory.create()
user1 = UserFactory.create()
user2 = UserFactory.create()

form = CollaboratorsForm(
instance=experiment,
data={"collaborators": [user1.id, user2.id]},
request=self.request,
)
self.assertTrue(form.is_valid())
experiment = form.save()

self.assertEqual(set(experiment.subscribers.all()), {user1, user2})
changelog = experiment.changes.latest("changed_on")
self.assertEqual(changelog.changed_by, self.user)
self.assertIn("updated collaborators", changelog.message)

def test_collaborators_form_removes_subscribers(self):
user1 = UserFactory.create()
user2 = UserFactory.create()
experiment = NimbusExperimentFactory.create()
experiment.subscribers.set([user1, user2])

form = CollaboratorsForm(
instance=experiment, data={"collaborators": [user1.id]}, request=self.request
)
self.assertTrue(form.is_valid())
experiment = form.save()

self.assertEqual(list(experiment.subscribers.all()), [user1])

def test_collaborators_form_initial_value(self):
user1 = UserFactory.create()
user2 = UserFactory.create()
experiment = NimbusExperimentFactory.create()
experiment.subscribers.set([user1, user2])

form = CollaboratorsForm(instance=experiment, request=self.request)
self.assertEqual(set(form.fields["collaborators"].initial), {user1, user2})


class TestTagAssignForm(RequestFormTestCase):
def test_valid_form_assigns_tags(self):
experiment = NimbusExperimentFactory.create()
Expand Down
42 changes: 42 additions & 0 deletions experimenter/experimenter/nimbus_ui/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1323,6 +1323,48 @@ def test_unsubscribe_from_experiment(self):
self.assertNotIn(self.user, self.experiment.subscribers.all())
self.assertEqual(response.status_code, 200)

def test_update_collaborators(self):
from experimenter.openidc.tests.factories import UserFactory

user1 = UserFactory.create()
user2 = UserFactory.create()

self.assertNotIn(user1, self.experiment.subscribers.all())
self.assertNotIn(user2, self.experiment.subscribers.all())

response = self.client.post(
reverse(
"nimbus-ui-update-collaborators", kwargs={"slug": self.experiment.slug}
),
{"collaborators": [user1.id, user2.id]},
)

self.experiment.refresh_from_db()

self.assertIn(user1, self.experiment.subscribers.all())
self.assertIn(user2, self.experiment.subscribers.all())
self.assertEqual(response.status_code, 200)

def test_update_collaborators_removes_users(self):
from experimenter.openidc.tests.factories import UserFactory

user1 = UserFactory.create()
user2 = UserFactory.create()
self.experiment.subscribers.set([user1, user2])

response = self.client.post(
reverse(
"nimbus-ui-update-collaborators", kwargs={"slug": self.experiment.slug}
),
{"collaborators": [user1.id]},
)

self.experiment.refresh_from_db()

self.assertIn(user1, self.experiment.subscribers.all())
self.assertNotIn(user2, self.experiment.subscribers.all())
self.assertEqual(response.status_code, 200)

def test_ready_is_false_if_review_serializer_invalid(self):
experiment = NimbusExperimentFactory.create(
public_description="",
Expand Down
6 changes: 6 additions & 0 deletions experimenter/experimenter/nimbus_ui/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
CancelEndEnrollmentView,
CancelEndExperimentView,
CancelUpdateRolloutView,
CollaboratorsUpdateView,
DocumentationLinkCreateView,
DocumentationLinkDeleteView,
DraftToPreviewView,
Expand Down Expand Up @@ -191,6 +192,11 @@
UnsubscribeView.as_view(),
name="nimbus-ui-unsubscribe",
),
re_path(
r"^(?P<slug>[\w-]+)/update_collaborators/",
CollaboratorsUpdateView.as_view(),
name="nimbus-ui-update-collaborators",
),
re_path(
r"^(?P<slug>[\w-]+)/draft-to-preview/$",
DraftToPreviewView.as_view(),
Expand Down
35 changes: 31 additions & 4 deletions experimenter/experimenter/nimbus_ui/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
CancelEndEnrollmentForm,
CancelEndExperimentForm,
CancelUpdateRolloutForm,
CollaboratorsForm,
DocumentationLinkCreateForm,
DocumentationLinkDeleteForm,
DraftToPreviewForm,
Expand Down Expand Up @@ -323,6 +324,7 @@ def get_context_data(self, **kwargs):
context["promote_to_rollout_forms"] = NimbusExperimentPromoteToRolloutForm(
instance=self.object
)
context["collaborators_form"] = CollaboratorsForm(instance=self.object)
context["qa_edit_mode"] = self.request.GET.get("edit_qa_status") == "true"
context["takeaways_edit_mode"] = self.request.GET.get("edit_takeaways") == "true"
if context["qa_edit_mode"]:
Expand Down Expand Up @@ -585,18 +587,43 @@ def get_context_data(self, **kwargs):
return context


class CollaboratorsContextMixin:
template_name = "nimbus_experiments/subscribers_list.html"

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["collaborators_form"] = CollaboratorsForm(instance=self.object)
return context


class CollaboratorsUpdateView(
CollaboratorsContextMixin,
NimbusExperimentViewMixin,
RequestFormMixin,
RenderResponseMixin,
UpdateView,
):
form_class = CollaboratorsForm


class SubscribeView(
NimbusExperimentViewMixin, RequestFormMixin, RenderResponseMixin, UpdateView
CollaboratorsContextMixin,
NimbusExperimentViewMixin,
RequestFormMixin,
RenderResponseMixin,
UpdateView,
):
form_class = SubscribeForm
template_name = "nimbus_experiments/subscribers_list.html"


class UnsubscribeView(
NimbusExperimentViewMixin, RequestFormMixin, RenderResponseMixin, UpdateView
CollaboratorsContextMixin,
NimbusExperimentViewMixin,
RequestFormMixin,
RenderResponseMixin,
UpdateView,
):
form_class = UnsubscribeForm
template_name = "nimbus_experiments/subscribers_list.html"


class FeatureSubscribeView(RequestFormMixin, RenderResponseMixin, UpdateView):
Expand Down