Arthur hai 10 horas
pai
achega
eeb50d7328

+ 1 - 1
docs/models/dcim/site.md

@@ -61,7 +61,7 @@ X = longitude × (π / 180) × 6378137
 Y = ln(tan(π / 4 + latitude × π / 360)) × 6378137
 ```
 
-where `ln` is the natural logarithm. Because Web Mercator is undefined at the poles, latitudes beyond ±85.051129° are clamped to that bound.
+where `ln` is the natural logarithm. Because Web Mercator is undefined at the poles, latitudes beyond ±85.05113° are clamped to that bound.
 
 !!! note
     Web Mercator (EPSG:3857) differs from the ellipsoidal World Mercator projection (EPSG:3395). Ensure the target system expects EPSG:3857 coordinates.

+ 1 - 1
netbox/dcim/constants.py

@@ -15,7 +15,7 @@ DEVICETYPE_IMAGE_FORMATS = 'image/bmp,image/gif,image/jpeg,image/png,image/tiff,
 # WGS84 semi-major axis (meters), used as the sphere radius for Web Mercator (EPSG:3857)
 WEB_MERCATOR_EARTH_RADIUS = 6378137
 # Latitude bounds of Web Mercator, at which the projection is square
-WEB_MERCATOR_MAX_LATITUDE = 85.051129
+WEB_MERCATOR_MAX_LATITUDE = 85.05112877980659
 
 
 #

+ 17 - 0
netbox/dcim/tests/test_api.py

@@ -3110,6 +3110,23 @@ class DeviceTestCase(Mixins.FieldPrefetchMixin, APIViewTestCases.APIViewTestCase
 
         self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
 
+    def test_web_mercator_coordinates(self):
+        """
+        Web Mercator coordinates are calculated from the GPS coordinates and cannot be written.
+        """
+        self.add_permissions('dcim.change_device')
+        device = Device.objects.first()
+        data = {
+            'latitude': 40.7128,
+            'longitude': -74.006,
+            'mercator_x': 1,
+            'mercator_y': 1,
+        }
+        response = self.client.patch(self._get_detail_url(device), data, format='json', **self.header)
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertEqual(response.data['mercator_x'], -8238310.24)
+        self.assertEqual(response.data['mercator_y'], 4970071.58)
+
     def test_create_device_replicates_components(self):
         """
         Creating a device without specifying replicate_components should instantiate its device type's components.

+ 2 - 2
netbox/dcim/tests/test_models.py

@@ -147,11 +147,11 @@ class WebMercatorTestCase(TestCase):
     def test_web_mercator_coordinates_at_poles(self):
         site = Site(latitude=Decimal('90'), longitude=Decimal('180'))
         self.assertEqual(site.mercator_x, 20037508.34)
-        self.assertEqual(site.mercator_y, 20037508.63)
+        self.assertEqual(site.mercator_y, 20037508.34)
 
         site = Site(latitude=Decimal('-90'), longitude=Decimal('-180'))
         self.assertEqual(site.mercator_x, -20037508.34)
-        self.assertEqual(site.mercator_y, -20037508.63)
+        self.assertEqual(site.mercator_y, -20037508.34)
 
 
 class LocationTestCase(TestCase):

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

@@ -40,8 +40,8 @@ class SitePanel(panels.ObjectAttributesPanel):
     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()
-    mercator_x = attrs.NumericAttr('mercator_x', label=_('Web Mercator X'), copy_button=True)
-    mercator_y = attrs.NumericAttr('mercator_y', label=_('Web Mercator Y'), copy_button=True)
+    mercator_x = attrs.NumericAttr('mercator_x', label=_('Web Mercator X'), copy_button=True, localize=False)
+    mercator_y = attrs.NumericAttr('mercator_y', label=_('Web Mercator Y'), copy_button=True, localize=False)
 
 
 class LocationPanel(panels.NestedGroupObjectPanel):
@@ -122,8 +122,8 @@ class DevicePanel(panels.ObjectAttributesPanel):
     virtual_chassis = attrs.RelatedObjectAttr('virtual_chassis', linkify=True)
     parent_device = attrs.TemplatedAttr('parent_bay', template_name='dcim/device/attrs/parent_device.html')
     gps_coordinates = attrs.GPSCoordinatesAttr()
-    mercator_x = attrs.NumericAttr('mercator_x', label=_('Web Mercator X'), copy_button=True)
-    mercator_y = attrs.NumericAttr('mercator_y', label=_('Web Mercator Y'), copy_button=True)
+    mercator_x = attrs.NumericAttr('mercator_x', label=_('Web Mercator X'), copy_button=True, localize=False)
+    mercator_y = attrs.NumericAttr('mercator_y', label=_('Web Mercator Y'), copy_button=True, localize=False)
     tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
     description = attrs.TextAttr('description')
     cooling_method = attrs.ChoiceAttr('cooling_method')

+ 5 - 5
netbox/netbox/tests/test_search.py

@@ -222,7 +222,7 @@ class SearchBackendTestCase(TestCase):
             # Unnamed devices render as "<manufacturer> <model> (<pk>)"; prefixes display their scope (a site)
             for site in sites:
                 Device.objects.create(site=site, device_type=device_type, role=role, description='Batched')
-                Prefix.objects.create(prefix=f'10.0.{site.pk}.0/24', scope=site, description='Batched')
+                Prefix.objects.create(prefix=f'10.0.{all_sites.index(site)}.0/24', scope=site, description='Batched')
             search_backend.cache(Device.objects.all())
             search_backend.cache(Prefix.objects.all())
 
@@ -232,15 +232,15 @@ class SearchBackendTestCase(TestCase):
                 rendered = [(result.name, list(result.display_attrs.values())) for result in results]
             return rendered, len(queries)
 
-        sites = list(Site.objects.order_by('name'))
-        rendered, query_count = render_results(sites[:1])
+        all_sites = list(Site.objects.order_by('name'))
+        rendered, query_count = render_results(all_sites[:1])
         self.assertEqual(len(rendered), 2)
-        rendered, more_query_count = render_results(sites[1:])
+        rendered, more_query_count = render_results(all_sites[1:])
         self.assertEqual(len(rendered), 6)
         self.assertEqual(more_query_count, query_count)
 
         self.assertEqual(sum(name.startswith('Manufacturer 1 Device Type 1') for name, _ in rendered), 3)
-        self.assertCountEqual([attrs[0] for name, attrs in rendered if name.startswith('10.0.')], sites)
+        self.assertCountEqual([attrs[0] for name, attrs in rendered if name.startswith('10.0.')], all_sites)
 
 
 class DeferredCachingTestCase(RQQueueTestMixin, TestCase):

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

@@ -169,19 +169,22 @@ class NumericAttr(ObjectAttribute):
     Parameters:
          unit_accessor (str): Accessor for the unit of measurement to display alongside the value (if any)
          copy_button (bool): Set to True to include a copy-to-clipboard button
+         localize (bool): Set to False to render the value without locale-specific formatting
     """
     template_name = 'ui/attrs/numeric.html'
 
-    def __init__(self, *args, unit_accessor=None, copy_button=False, **kwargs):
+    def __init__(self, *args, unit_accessor=None, copy_button=False, localize=True, **kwargs):
         super().__init__(*args, **kwargs)
         self.unit_accessor = unit_accessor
         self.copy_button = copy_button
+        self.localize = localize
 
     def get_context(self, obj, attr, value, context):
         unit = resolve_attr_path(obj, self.unit_accessor) if self.unit_accessor else None
         return {
             'unit': unit,
             'copy_button': self.copy_button,
+            'localize': self.localize,
         }
 
 

+ 2 - 2
netbox/templates/ui/attrs/numeric.html

@@ -1,6 +1,6 @@
-{% load i18n %}
+{% load i18n l10n %}
 <span>
-  <span{% if name %} id="attr_{{ name }}"{% endif %}>{{ value }}</span>
+  <span{% if name %} id="attr_{{ name }}"{% endif %}>{% if localize %}{{ value }}{% else %}{{ value|unlocalize }}{% endif %}</span>
   {% if unit %}
     {{ unit|lower }}
   {% endif %}