Repository: nautobot/nautobot

Issue:
Cloning objects with special characters in a clone_field
<!--
    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
* Nautobot version (Docker tag too if applicable): 2.2.2
* Python version: 3.10.12
* Database platform, version: postgres, 16.2
* Middleware(s):

<!--
    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. Set a field that is cloned on an object to contain a value that needs to be URL escaped (for instance a Location with the Description "9th & Vine")
![NautobotCloneIssue-0000](https://github.com/nautobot/nautobot/assets/635290/00111b7f-7402-484b-9bcf-3f754268dee8)
3. Press the Clone button on the environment

<!-- What did you expect to happen? -->
### Expected Behavior
The URL for creating the clone instance would be: https://nautobot.example.com/dcim/locations/add/?location_type=c1c796ba-fc7c-4f4c-8c03-a9378d220890&status=b43753f4-8814-47ad-a780-2883924eb795&parent=64965d9d-3fb5-4ae1-9a22-03937a1a3653&description=9th%20%26%20Vine

The Description field contains the value "9th & Vine"

<!-- What happened instead? -->
### Observed Behavior
The URL created is The URL for creating the clone instance would be: https://nautobot.example.com/dcim/locations/add/?location_type=c1c796ba-fc7c-4f4c-8c03-a9378d220890&status=b43753f4-8814-47ad-a780-2883924eb795&parent=64965d9d-3fb5-4ae1-9a22-03937a1a3653&description=9th%20&%20Vine

The Description field contains the value "9th "
![NautobotCloneIssue-0001](https://github.com/nautobot/nautobot/assets/635290/4bbec75a-b2ae-4c6a-9926-1ad599584457)


Discussion:
I wanted to also comment, that I have a unit test and a PR I'll be generating shortly to fix this bug. It was impacting a production environment, so we had already patched our installs.
Thank you @jdueitt - that's the best kind of bug report, one that already has a fix ready! 😃 

Resolving patch:
diff --git a/nautobot/core/views/utils.py b/nautobot/core/views/utils.py
--- a/nautobot/core/views/utils.py
+++ b/nautobot/core/views/utils.py
@@ -1,5 +1,6 @@
 import datetime
 from io import BytesIO
+import urllib.parse
 
 from django.contrib import messages
 from django.core.exceptions import FieldError, ValidationError
@@ -269,7 +270,7 @@ def prepare_cloned_fields(instance):
         for tag in instance.tags.all():
             params.append(("tags", tag.pk))
 
-    # Concatenate parameters into a URL query string
-    param_string = "&".join([f"{k}={v}" for k, v in params])
+    # Encode the parameters into a URL query string
+    param_string = urllib.parse.urlencode(params)
 
     return param_string


Test patch:
diff --git a/nautobot/core/tests/test_views_utils.py b/nautobot/core/tests/test_views_utils.py
--- a/nautobot/core/tests/test_views_utils.py
+++ b/nautobot/core/tests/test_views_utils.py
@@ -1,8 +1,10 @@
+import urllib.parse
+
 from django.db import ProgrammingError
 from django.test import TestCase
 
 from nautobot.core.models.querysets import count_related
-from nautobot.core.views.utils import check_filter_for_display
+from nautobot.core.views.utils import check_filter_for_display, prepare_cloned_fields
 from nautobot.dcim.filters import DeviceFilterSet
 from nautobot.dcim.models import Device, DeviceRedundancyGroup, DeviceType, InventoryItem, Location, Manufacturer
 from nautobot.extras.models import Role, Status
@@ -147,3 +149,22 @@ def test_count_related(self):
             manufacturer_count=count_related(Manufacturer, "inventory_items__device", distinct=True)
         )
         self.assertEqual(qs.get(pk=device1.pk).manufacturer_count, 3)
+
+
+class CheckPrepareClonedFields(TestCase):
+    name = "Building-02"
+    descriptions = ["Complicated Name & Stuff", "Simple Name"]
+
+    def testQueryParameterGeneration(self):
+        """Assert that a clone field with a special character, &, is properly escaped"""
+        instance = Location.objects.get(name=self.name)
+        self.assertIsInstance(instance, Location)
+        for description in self.descriptions:
+            with self.subTest(f"Testing parameter generation for a model with the name '{description}'"):
+                instance.description = description
+                query_params = urllib.parse.parse_qs(prepare_cloned_fields(instance))
+                self.assertTrue(isinstance(query_params, dict))
+                self.assertTrue("description" in query_params.keys())
+                self.assertTrue(isinstance(query_params["description"], list))
+                self.assertTrue(len(query_params["description"]) == 1)
+                self.assertTrue(query_params["description"][0] == description)