Repository: netbox-community/netbox

Issue:
Custom Script MultiObjectVar cannot be filled from URL parameter
<!--
    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.

    This form is only for reproducible bugs. If you need assistance with
    NetBox installation, or if you have a general question, DO NOT open an
    issue. Instead, post to our mailing list:

        https://groups.google.com/forum/#!forum/netbox-discuss

    Please describe the environment in which you are running NetBox. Be sure
    that you are running an unmodified instance of the latest stable release
    before submitting a bug report, and that any plugins have been disabled.
-->
### Environment
* Python version: 3.7
* NetBox version: 2.7.12

I'm building a custom script with multiple fields that I want to pre-fill from a a URL (that I will generate from a Custom Link).
It works, except for MultiObjectVar (Multiple Select) variable type.

### Steps to Reproduce
1. Create Custom Script with a MultiObjectVar field (named "test")
```
test = MultiObjectVar(
    queryset=Device.objects.filter(status="active"),
    required=True,
    label="Test field", 
)
```
2. Try to put a valid device ID in URL parameter ( http://netbox/extra/scripts/myscript/Myclass/?test=1 )
3. Or list-type format (?test=[1,2])

<!-- What did you expect to happen? -->
### Expected Behavior
Field should contain the value of ID in specified in URL parameter test (GET)

<!-- What happened instead? -->
### Observed Behavior
with step 1 and 2: It doesn't fill any field. MultiObjectVar is blank/null. No error.

With step 3, as a MultiObjectVar should contain a list object, it gives:
class 'ValueError'
invalid literal for int() with base 10: ']'

If I add "default=[1,2]" to step 1 MultiObjectVar, it will fill it with the right values.
"default", for this MultiObjectVar variable, needs a list object. int() will give an error.

If you replace MultiObjectVar with ObjectVar and repeat step 2, it will fill the field with the right value.

Filling fields from GET parameters seems to work for all other field type I tried.

We should be able to pass multiple values using GET requests for such field type.


Discussion:
Is `?test=[1,2]` the syntax we want for this? I personally would prefer to make the brackets optional and also allow `?test=1,2` and `?test=1`. Any objections to that?
Let’s try to keep it consistent between other API filters? I think the rest api requires test=1&test=2 to dictate an ‘or’ filter?

edit: https://github.com/netbox-community/netbox/issues/4313
> edit: #4313

That makes the choice very clear :)  Working on it!

Resolving patch:
diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py
--- a/netbox/extras/forms.py
+++ b/netbox/extras/forms.py
@@ -432,11 +432,11 @@ class ScriptForm(BootstrapMixin, forms.Form):
 
     def __init__(self, vars, *args, commit_default=True, **kwargs):
 
-        super().__init__(*args, **kwargs)
-
         # Dynamically populate fields for variables
         for name, var in vars.items():
-            self.fields[name] = var.as_field()
+            self.base_fields[name] = var.as_field()
+
+        super().__init__(*args, **kwargs)
 
         # Toggle default commit behavior based on Meta option
         if not commit_default:
diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py
--- a/netbox/utilities/forms.py
+++ b/netbox/utilities/forms.py
@@ -606,15 +606,18 @@ class DynamicModelChoiceMixin:
     filter = django_filters.ModelChoiceFilter
     widget = APISelect
 
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
+    def _get_initial_value(self, initial_data, field_name):
+        return initial_data.get(field_name)
 
     def get_bound_field(self, form, field_name):
         bound_field = BoundField(form, self, field_name)
 
+        # Override initial() to allow passing multiple values
+        bound_field.initial = self._get_initial_value(form.initial, field_name)
+
         # Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options
         # will be populated on-demand via the APISelect widget.
-        data = self.prepare_value(bound_field.data or bound_field.initial)
+        data = bound_field.value()
         if data:
             filter = self.filter(field_name=self.to_field_name or 'pk', queryset=self.queryset)
             self.queryset = filter.filter(self.queryset, data)
@@ -647,6 +650,12 @@ class DynamicModelMultipleChoiceField(DynamicModelChoiceMixin, forms.ModelMultip
     filter = django_filters.ModelMultipleChoiceFilter
     widget = APISelectMultiple
 
+    def _get_initial_value(self, initial_data, field_name):
+        # If a QueryDict has been passed as initial form data, get *all* listed values
+        if hasattr(initial_data, 'getlist'):
+            return initial_data.getlist(field_name)
+        return initial_data.get(field_name)
+
 
 class LaxURLField(forms.URLField):
     """


Test patch: