Repository: nautobot/nautobot

Issue:
Missing Vrf fields in bulk edit menu
### Environment
* Nautobot version (Docker tag too if applicable): 2.2.1
* Python version:
* Database platform, version:
* Middleware(s):


### Steps to Reproduce

1.    Select multiple prefixes
2.    Click on Edit selected
3.    Try to adding or removing Vrfs

### Expected Behavior

Fields for adding and removing vrfs should be presented, similar to the "Add tags" and "Remove tags" fields.

### Observed Behavior
The "Add VRFs" and "Remove VRFs" fields are missing from bulk edit.


Discussion:

Resolving patch:
diff --git a/nautobot/ipam/forms.py b/nautobot/ipam/forms.py
--- a/nautobot/ipam/forms.py
+++ b/nautobot/ipam/forms.py
@@ -142,6 +142,12 @@ class VRFBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm):
     namespace = DynamicModelChoiceField(queryset=Namespace.objects.all(), required=False)
     tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False)
     description = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
+    add_prefixes = DynamicModelMultipleChoiceField(
+        queryset=Prefix.objects.all(), required=False, query_params={"namespace": "$namespace"}
+    )
+    remove_prefixes = DynamicModelMultipleChoiceField(
+        queryset=Prefix.objects.all(), required=False, query_params={"namespace": "$namespace"}
+    )
 
     class Meta:
         nullable_fields = [
@@ -358,6 +364,12 @@ class PrefixBulkEditForm(
     remove_locations = DynamicModelMultipleChoiceField(
         queryset=Location.objects.all(), required=False, query_params={"content_type": Prefix._meta.label_lower}
     )
+    add_vrfs = DynamicModelMultipleChoiceField(
+        queryset=VRF.objects.all(), required=False, query_params={"namespace": "$namespace"}
+    )
+    remove_vrfs = DynamicModelMultipleChoiceField(
+        queryset=VRF.objects.all(), required=False, query_params={"namespace": "$namespace"}
+    )
     tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False)
     rir = DynamicModelChoiceField(queryset=RIR.objects.all(), required=False, label="RIR")
     date_allocated = forms.DateTimeField(required=False, widget=DateTimePicker)
diff --git a/nautobot/ipam/views.py b/nautobot/ipam/views.py
--- a/nautobot/ipam/views.py
+++ b/nautobot/ipam/views.py
@@ -303,6 +303,12 @@ class VRFBulkEditView(generic.BulkEditView):
     table = tables.VRFTable
     form = forms.VRFBulkEditForm
 
+    def extra_post_save_action(self, obj, form):
+        if form.cleaned_data.get("add_prefixes", None):
+            obj.prefixes.add(*form.cleaned_data["add_prefixes"])
+        if form.cleaned_data.get("remove_prefixes", None):
+            obj.prefixes.remove(*form.cleaned_data["remove_prefixes"])
+
 
 class VRFBulkDeleteView(generic.BulkDeleteView):
     queryset = VRF.objects.select_related("tenant")
@@ -713,6 +719,10 @@ def extra_post_save_action(self, obj, form):
             obj.locations.add(*form.cleaned_data["add_locations"])
         if form.cleaned_data.get("remove_locations", None):
             obj.locations.remove(*form.cleaned_data["remove_locations"])
+        if form.cleaned_data.get("add_vrfs", None):
+            obj.vrfs.add(*form.cleaned_data["add_vrfs"])
+        if form.cleaned_data.get("remove_vrfs", None):
+            obj.vrfs.remove(*form.cleaned_data["remove_vrfs"])
 
 
 class PrefixBulkDeleteView(generic.BulkDeleteView):


Test patch:
diff --git a/nautobot/ipam/tests/test_views.py b/nautobot/ipam/tests/test_views.py
--- a/nautobot/ipam/tests/test_views.py
+++ b/nautobot/ipam/tests/test_views.py
@@ -71,7 +71,8 @@ class VRFTestCase(ViewTestCases.PrimaryObjectViewTestCase):
     @classmethod
     def setUpTestData(cls):
         tenants = Tenant.objects.all()[:2]
-        namespace = Namespace.objects.create(name="ipam_test_views_vrf_test")
+        namespace = Prefix.objects.first().namespace
+        prefixes = Prefix.objects.filter(namespace=namespace)
 
         cls.form_data = {
             "name": "VRF X",
@@ -79,12 +80,16 @@ def setUpTestData(cls):
             "rd": "65000:999",
             "tenant": tenants[0].pk,
             "description": "A new VRF",
+            "prefixes": [prefixes[1].id],
             "tags": [t.pk for t in Tag.objects.get_for_model(VRF)],
         }
 
         cls.bulk_edit_data = {
             "tenant": tenants[1].pk,
             "description": "New description",
+            "namespace": prefixes[0].namespace.id,
+            "add_prefixes": [prefixes[0].id],
+            "remove_prefixes": [prefixes[1].id],
         }
 
 
@@ -158,7 +163,6 @@ def setUpTestData(cls):
 
         cls.bulk_edit_data = {
             "tenant": None,
-            # TODO "vrf": vrfs[1].pk,
             "status": cls.statuses[1].pk,
             "role": cls.roles[1].pk,
             "rir": RIR.objects.last().pk,
@@ -166,6 +170,9 @@ def setUpTestData(cls):
             "description": "New description",
             "add_locations": [cls.locations[0].pk],
             "remove_locations": [cls.locations[1].pk],
+            "namespace": vrfs[0].namespace.pk,
+            "add_vrfs": [vrfs[0].pk],
+            "remove_vrfs": [vrfs[1].pk],
         }
 
     @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])