Repository: nautobot/nautobot

Issue:
Add the ability for RU numbers to display leading zeros
### As ...

Nelly - Network Engineer

### I want ...

Rack units to have leading zeros in the Rack Detail View

### So that ...

The elevation view better represents the real-world label that is on my rack

### I know this is done when...

Rack units labels will display 01, 02, 03, ... instead of 1, 2, 3, ...

### Optional - Feature groups this request pertains to.

- [ ] Automation
- [ ] Circuits
- [X] DCIM
- [ ] IPAM
- [ ] Misc (including Data Sources)
- [ ] Organization
- [ ] Plugins (and other Extensibility)
- [ ] Security (Secrets, etc)
- [ ] Image Management
- [X] UI/UX
- [ ] Documentation
- [ ] Other (not directly a platform feature)

### Database Changes

Should not require a DB change, just display format.

### External Dependencies

None


Discussion:
I will work on this.

Resolving patch:
diff --git a/nautobot/core/settings.py b/nautobot/core/settings.py
--- a/nautobot/core/settings.py
+++ b/nautobot/core/settings.py
@@ -188,6 +188,13 @@
 ):
     RACK_ELEVATION_DEFAULT_UNIT_WIDTH = int(os.environ["NAUTOBOT_RACK_ELEVATION_DEFAULT_UNIT_WIDTH"])
 
+# Enable two-digit format for the rack unit numbering in rack elevations.
+if (
+    "NAUTOBOT_RACK_ELEVATION_UNIT_TWO_DIGIT_FORMAT" in os.environ
+    and os.environ["NAUTOBOT_RACK_ELEVATION_UNIT_TWO_DIGIT_FORMAT"] != ""
+):
+    RACK_ELEVATION_UNIT_TWO_DIGIT_FORMAT = is_truthy(os.environ["NAUTOBOT_RACK_ELEVATION_UNIT_TWO_DIGIT_FORMAT"])
+
 # How frequently to check for a new Nautobot release on GitHub, and the URL to check for this information.
 # Defaults to disabled (no URL) and check every 24 hours when enabled
 if "NAUTOBOT_RELEASE_CHECK_TIMEOUT" in os.environ and os.environ["NAUTOBOT_RELEASE_CHECK_TIMEOUT"] != "":
@@ -769,6 +776,11 @@
     "RACK_ELEVATION_DEFAULT_UNIT_WIDTH": ConstanceConfigItem(
         default=230, help_text="Default width (in pixels) of a rack unit in a rack elevation diagram", field_type=int
     ),
+    "RACK_ELEVATION_UNIT_TWO_DIGIT_FORMAT": ConstanceConfigItem(
+        default=False,
+        help_text="Enables two-digit format for the rack unit numbering in a rack elevation diagram",
+        field_type=bool,
+    ),
     "RELEASE_CHECK_TIMEOUT": ConstanceConfigItem(
         default=24 * 3600,
         help_text="Number of seconds (must be at least 3600, or one hour) to cache the result of a release check "
@@ -800,7 +812,11 @@
     "Natural Keys": ["DEVICE_NAME_AS_NATURAL_KEY", "LOCATION_NAME_AS_NATURAL_KEY"],
     "Pagination": ["PAGINATE_COUNT", "MAX_PAGE_SIZE", "PER_PAGE_DEFAULTS"],
     "Performance": ["DYNAMIC_GROUPS_MEMBER_CACHE_TIMEOUT", "JOB_CREATE_FILE_MAX_SIZE"],
-    "Rack Elevation Rendering": ["RACK_ELEVATION_DEFAULT_UNIT_HEIGHT", "RACK_ELEVATION_DEFAULT_UNIT_WIDTH"],
+    "Rack Elevation Rendering": [
+        "RACK_ELEVATION_DEFAULT_UNIT_HEIGHT",
+        "RACK_ELEVATION_DEFAULT_UNIT_WIDTH",
+        "RACK_ELEVATION_UNIT_TWO_DIGIT_FORMAT",
+    ],
     "Release Checking": ["RELEASE_CHECK_URL", "RELEASE_CHECK_TIMEOUT"],
     "User Interface": ["SUPPORT_MESSAGE"],
     "Debugging": ["ALLOW_REQUEST_PROFILING"],
diff --git a/nautobot/dcim/elevations.py b/nautobot/dcim/elevations.py
--- a/nautobot/dcim/elevations.py
+++ b/nautobot/dcim/elevations.py
@@ -3,6 +3,8 @@
 from django.utils.http import urlencode
 import svgwrite
 
+from nautobot.core.utils.config import get_settings_or_config
+
 from .choices import DeviceFaceChoices
 from .constants import RACK_ELEVATION_BORDER_WIDTH
 
@@ -233,7 +235,9 @@ def render(self, face, unit_width, unit_height, legend_width):
                 start_y + unit_height / 2 + RACK_ELEVATION_BORDER_WIDTH,
             )
             unit = ru + 1 if self.rack.desc_units else self.rack.u_height - ru
-            drawing.add(drawing.text(str(unit), position_coordinates, class_="unit"))
+            unit_two_digit_format = get_settings_or_config("RACK_ELEVATION_UNIT_TWO_DIGIT_FORMAT")
+            unit_display = f"{unit:02d}" if unit_two_digit_format else str(unit)
+            drawing.add(drawing.text(unit_display, position_coordinates, class_="unit"))
 
         for unit in self.merge_elevations(face):
             # Loop through all units in the elevation


Test patch:
diff --git a/nautobot/dcim/tests/test_api.py b/nautobot/dcim/tests/test_api.py
--- a/nautobot/dcim/tests/test_api.py
+++ b/nautobot/dcim/tests/test_api.py
@@ -618,6 +618,42 @@ def test_get_rack_elevation_svg_config_overridden(self):
         self.assertEqual(response.get("Content-Type"), "image/svg+xml")
         self.assertIn(b'class="slot" height="19" width="190"', response.content)
 
+    @override_settings(
+        RACK_ELEVATION_UNIT_TWO_DIGIT_FORMAT=False,
+        RACK_ELEVATION_DEFAULT_UNIT_HEIGHT=22,
+        RACK_ELEVATION_DEFAULT_UNIT_WIDTH=230,
+    )
+    @override_config(RACK_ELEVATION_UNIT_TWO_DIGIT_FORMAT=True)
+    def test_get_rack_elevation_unit_svg_settings_overridden(self):
+        """
+        GET a single rack elevation in SVG format, with Django settings specifying the default RU display format
+        """
+        rack = Rack.objects.first()
+        self.add_permissions("dcim.view_rack")
+        reverse_url = reverse("dcim-api:rack-elevation", kwargs={"pk": rack.pk})
+        url = f"{reverse_url}?render=svg"
+
+        response = self.client.get(url, **self.header)
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertEqual(response.get("Content-Type"), "image/svg+xml")
+        self.assertIn(b'<text class="unit" x="15.0" y="915.0">1</text>', response.content)
+
+    @override_settings(RACK_ELEVATION_DEFAULT_UNIT_HEIGHT=22, RACK_ELEVATION_DEFAULT_UNIT_WIDTH=230)
+    @override_config(RACK_ELEVATION_UNIT_TWO_DIGIT_FORMAT=True)
+    def test_get_rack_elevation_unit_svg_config_overridden(self):
+        """
+        GET a single rack elevation in SVG format, with Constance config specifying the 2-digit RU display format
+        """
+        rack = Rack.objects.first()
+        self.add_permissions("dcim.view_rack")
+        reverse_url = reverse("dcim-api:rack-elevation", kwargs={"pk": rack.pk})
+        url = f"{reverse_url}?render=svg"
+
+        response = self.client.get(url, **self.header)
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertEqual(response.get("Content-Type"), "image/svg+xml")
+        self.assertIn(b'<text class="unit" x="15.0" y="915.0">01</text>', response.content)
+
     def test_detail_view_schema(self):
         url = self._get_detail_url(self._get_queryset().first())
         response = self.client.options(url, **self.header)