Repository: nautobot/nautobot

Issue:
Cloning Device or IP with a tag applied results in a Server Error
<!--
    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.

    This form is only for reporting reproducible bugs. If you need assistance
    with Nautobot installation, or if you have a general question, please start a
    discussion instead: https://github.com/nautobot/nautobot/discussions

    Please describe the environment in which you are running Nautobot. 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
* Ubuntu 20.04
* Python version: 3.8.5
* Nautobot version: 1.0.2

<!--
    Describe in detail the exact steps that someone else can take to reproduce
    this bug using the current stable release of Nautobot. Begin with the
    creation of any necessary database objects and call out every operation
    being performed explicitly. If reporting a bug in the REST API, be sure to
    reconstruct the raw HTTP request(s) being made: Don't rely on a client
    library such as pynautobot.
-->
### Steps to Reproduce
1. Install NautoBot 1.0.2 per the directions on https://nautobot.readthedocs.io
2. Create a Site, Manufacturer, Device Role, Tag, and then a Device with all applied
3. Click clone on the created device and receive a Server Error
4. Remove the tag from the device (so there are no tags at all) click Clone and it works
5. Issue also occurs with IPs

<!-- What did you expect to happen? -->
### Expected Behavior
Clone button works without error allowing the device to be cloned

<!-- What happened instead? -->
### Observed Behavior
Server Error like the following - the invalid UUID is not always 'a', it changes alphabet or number

 Server Error

There was a problem with your request. Please contact an administrator.

The complete exception is provided below:

<class 'django.core.exceptions.ValidationError'>

['“a” is not a valid UUID.']

Python version: 3.8.5
Nautobot version: 1.0.2

If further assistance is required, please post to the #nautobot channel on NetworkToCode's Slack. 




Discussion:
Thanks for the report! This may be related to #499.
Looks to be an issue with pre-populating values for any form that uses a `DynamicModelMultipleChoiceField`. Continuing to investigate.

Resolving patch:
diff --git a/nautobot/utilities/forms/fields.py b/nautobot/utilities/forms/fields.py
--- a/nautobot/utilities/forms/fields.py
+++ b/nautobot/utilities/forms/fields.py
@@ -477,6 +477,20 @@ class DynamicModelMultipleChoiceField(DynamicModelChoiceMixin, forms.ModelMultip
     filter = django_filters.ModelMultipleChoiceFilter
     widget = widgets.APISelectMultiple
 
+    def prepare_value(self, value):
+        """
+        Ensure that a single string value (i.e. UUID) is accurately represented as a list of one item.
+
+        This is necessary because otherwise the superclass will split the string into individual characters,
+        resulting in an error (https://github.com/nautobot/nautobot/issues/512).
+
+        Note that prepare_value() can also be called with an object instance or list of instances; in that case,
+        we do *not* want to convert a single instance to a list of one entry.
+        """
+        if isinstance(value, str):
+            value = [value]
+        return super().prepare_value(value)
+
 
 class LaxURLField(forms.URLField):
     """


Test patch:
diff --git a/nautobot/utilities/tests/test_forms.py b/nautobot/utilities/tests/test_forms.py
--- a/nautobot/utilities/tests/test_forms.py
+++ b/nautobot/utilities/tests/test_forms.py
@@ -5,7 +5,7 @@
 
 from nautobot.ipam.forms import IPAddressCSVForm, ServiceForm
 from nautobot.ipam.models import IPAddress, Prefix
-from nautobot.utilities.forms.fields import CSVDataField, NumericArrayField
+from nautobot.utilities.forms.fields import CSVDataField, DynamicModelMultipleChoiceField, NumericArrayField
 from nautobot.utilities.forms.utils import (
     expand_alphanumeric_pattern,
     expand_ipaddress_pattern,
@@ -411,6 +411,44 @@ def test_clean_to_field_on_non_object(self):
             self.field.clean(input)
 
 
+class DynamicModelMultipleChoiceFieldTest(TestCase):
+    """Tests for DynamicModelMultipleChoiceField."""
+
+    def setUp(self):
+        self.field = DynamicModelMultipleChoiceField(queryset=IPAddress.objects.all())
+
+    def test_prepare_value_single_str(self):
+        """A single string (UUID) value should be treated as a single-entry list."""
+        self.assertEqual(
+            self.field.prepare_value("c671a001-4c17-4ca1-80fd-fe1609bcadec"),
+            ["c671a001-4c17-4ca1-80fd-fe1609bcadec"],
+        )
+
+    def test_prepare_value_multiple_str(self):
+        """A list of string (UUID) values should be handled as-is."""
+        self.assertEqual(
+            self.field.prepare_value(["c671a001-4c17-4ca1-80fd-fe1609bcadec", "097581e8-1fd5-444f-bbf4-46324e924826"]),
+            ["c671a001-4c17-4ca1-80fd-fe1609bcadec", "097581e8-1fd5-444f-bbf4-46324e924826"],
+        )
+
+    def test_prepare_value_single_object(self):
+        """A single object value should be translated to its corresponding PK."""
+        address = IPAddress.objects.create(address="10.1.1.1/24")
+        self.assertEqual(
+            self.field.prepare_value(address),
+            address.pk,
+        )
+
+    def test_prepare_value_multiple_object(self):
+        """A list of object values should be translated to a list of PKs."""
+        address_1 = IPAddress.objects.create(address="10.1.1.1/24")
+        address_2 = IPAddress.objects.create(address="10.1.1.2/24")
+        self.assertEqual(
+            self.field.prepare_value([address_1, address_2]),
+            [address_1.pk, address_2.pk],
+        )
+
+
 class NumericArrayFieldTest(TestCase):
     def setUp(self):
         super().setUp()