Просмотр исходного кода

Closes #23365: Add copy-to-clipboard button for site addresses

Carlos Alberto R G 16 часов назад
Родитель
Сommit
1d270e5746

+ 13 - 0
netbox/dcim/tests/test_views.py

@@ -233,6 +233,19 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             with self.subTest(panel=panel):
                 self.assertNotContains(response, url)
 
+    def test_get_object_address_copy_buttons(self):
+        """The physical and shipping addresses each render a copy-to-clipboard button."""
+        self.add_permissions('dcim.view_site')
+        site = self._get_queryset().first()
+        site.physical_address = '742 Evergreen Terrace\nSpringfield, USA'
+        site.shipping_address = '1 Loading Dock Way\nSpringfield, USA'
+        site.save()
+
+        response = self.client.get(site.get_absolute_url())
+        self.assertHttpStatus(response, 200)
+        self.assertContains(response, 'data-clipboard-text="742 Evergreen Terrace\nSpringfield, USA"')
+        self.assertContains(response, 'data-clipboard-text="1 Loading Dock Way\nSpringfield, USA"')
+
     def test_get_object_vlan_row_covers_group_scopes(self):
         """The VLANs row counts direct, site-scoped and site-group-scoped VLANs and links to the filter."""
         self.add_permissions('dcim.view_site', 'ipam.view_vlan')

+ 2 - 2
netbox/dcim/ui/panels.py

@@ -37,8 +37,8 @@ class SitePanel(panels.ObjectAttributesPanel):
     facility = attrs.TextAttr('facility')
     description = attrs.TextAttr('description')
     timezone = attrs.TimezoneAttr('time_zone')
-    physical_address = attrs.AddressAttr('physical_address', map_url=True)
-    shipping_address = attrs.AddressAttr('shipping_address', map_url=True)
+    physical_address = attrs.AddressAttr('physical_address', map_url=True, copy_button=True)
+    shipping_address = attrs.AddressAttr('shipping_address', map_url=True, copy_button=True)
     gps_coordinates = attrs.GPSCoordinatesAttr()
 
 

+ 58 - 0
netbox/netbox/tests/test_ui.py

@@ -578,6 +578,64 @@ class AddressAttrTestCase(TestCase):
         context = attr.get_context(obj, 'address', '1 Main St', {})
         self.assertIsNone(context['map_url'])
 
+    def test_copy_button_disabled_by_default(self):
+        attr = attrs.AddressAttr('address')
+        obj = SimpleNamespace(address='1 Main St')
+        context = attr.get_context(obj, 'address', '1 Main St', {})
+        self.assertFalse(context['copy_button'])
+
+    def test_get_context_copy_button(self):
+        attr = attrs.AddressAttr('address', copy_button=True)
+        obj = SimpleNamespace(address='1 Main St')
+        context = attr.get_context(obj, 'address', '1 Main St', {})
+        self.assertTrue(context['copy_button'])
+
+    def test_render_with_copy_button(self):
+        attr = attrs.AddressAttr('address', map_url=False, copy_button=True)
+        obj = SimpleNamespace(address='1 Main St\nSuite 100')
+        rendered = attr.render(obj, {'name': 'address'})
+        self.assertIn('copy-content', rendered)
+        # The raw value (line breaks included) is copied, rather than a selection of the rendered HTML
+        self.assertIn('data-clipboard-text="1 Main St\nSuite 100"', rendered)
+        self.assertNotIn('data-clipboard-target', rendered)
+
+    def test_render_copy_button_hidden_when_printing(self):
+        attr = attrs.AddressAttr('address', map_url=False, copy_button=True)
+        obj = SimpleNamespace(address='1 Main St')
+        rendered = attr.render(obj, {'name': 'address'})
+        self.assertIn('d-print-none', rendered)
+
+    def test_render_copy_button_escapes_value(self):
+        attr = attrs.AddressAttr('address', map_url=False, copy_button=True)
+        obj = SimpleNamespace(address='O\'Brien & Sons "HQ" <Suite 5>')
+        rendered = attr.render(obj, {'name': 'address'})
+        self.assertIn(
+            'data-clipboard-text="O&#x27;Brien &amp; Sons &quot;HQ&quot; &lt;Suite 5&gt;"',
+            rendered,
+        )
+        self.assertNotIn('<Suite 5>', rendered)
+
+    def test_render_without_copy_button(self):
+        attr = attrs.AddressAttr('address', map_url=False)
+        obj = SimpleNamespace(address='1 Main St\nSuite 100')
+        rendered = attr.render(obj, {'name': 'address'})
+        self.assertNotIn('copy-content', rendered)
+        self.assertNotIn('data-clipboard-text', rendered)
+
+    def test_render_map_link_without_copy_button(self):
+        attr = attrs.AddressAttr('address', map_url='https://maps.google.com/?q=')
+        obj = SimpleNamespace(address='1 Main St')
+        rendered = attr.render(obj, {'name': 'address'})
+        self.assertIn('href="https://maps.google.com/?q=1 Main St"', rendered)
+        self.assertNotIn('copy-content', rendered)
+
+    def test_render_map_link_and_copy_button(self):
+        attr = attrs.AddressAttr('address', map_url='https://maps.google.com/?q=', copy_button=True)
+        obj = SimpleNamespace(address='1 Main St')
+        rendered = attr.render(obj, {'name': 'address'})
+        self.assertIn('href="https://maps.google.com/?q=1 Main St"', rendered)
+        self.assertIn('data-clipboard-text="1 Main St"', rendered)
+
 
 class DateTimeAttrTestCase(TestCase):
 

+ 4 - 1
netbox/netbox/ui/attrs.py

@@ -488,12 +488,14 @@ class AddressAttr(MapURLMixin, ObjectAttribute):
     Parameters:
          map_url (bool/str): The URL to use when rendering the address. If True, the address will render as a
               hyperlink using settings.MAPS_URL.
+         copy_button (bool): Set to True to include a copy-to-clipboard button
     """
     template_name = 'ui/attrs/address.html'
 
-    def __init__(self, *args, map_url=True, **kwargs):
+    def __init__(self, *args, map_url=True, copy_button=False, **kwargs):
         super().__init__(*args, **kwargs)
         self._map_url = map_url
+        self.copy_button = copy_button
 
     def get_context(self, obj, attr, value, context):
         map_url = self.map_url
@@ -502,6 +504,7 @@ class AddressAttr(MapURLMixin, ObjectAttribute):
             map_url = None
         return {
             'map_url': map_url,
+            'copy_button': self.copy_button,
         }
 
 

+ 13 - 4
netbox/templates/ui/attrs/address.html

@@ -1,8 +1,17 @@
 {% load i18n %}
 {% load l10n %}
 <span>{{ value|linebreaksbr }}</span>
-{% if map_url %}
-  <a href="{{ map_url }}{{ value }}" target="_blank" class="btn btn-primary btn-sm print-none">
-    <i class="mdi mdi-map-marker"></i> {% trans "Map" %}
-  </a>
+{% if map_url or copy_button %}
+  <div class="d-flex gap-1">
+    {% if map_url %}
+      <a href="{{ map_url }}{{ value }}" target="_blank" class="btn btn-primary btn-sm print-none">
+        <i class="mdi mdi-map-marker"></i> {% trans "Map" %}
+      </a>
+    {% endif %}
+    {% if copy_button %}
+      <a class="btn btn-sm btn-primary copy-content d-print-none" data-clipboard-text="{{ value }}" title="{% trans "Copy to clipboard" %}">
+        <i class="mdi mdi-content-copy"></i>
+      </a>
+    {% endif %}
+  </div>
 {% endif %}