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

Display the duration for running scripts

Jeremy Stretch 2 недель назад
Родитель
Сommit
cd7fdf2267

+ 7 - 3
netbox/templates/extras/htmx/script_result.html

@@ -11,9 +11,13 @@
     {% else %}
       {% trans "Created" %}: <strong>{{ job.created|isodatetime }}</strong>
     {% endif %}
-    {% if job.completed %}
-      {% trans "Duration" %}: <strong>{{ job.duration }}</strong>
-    {% endif %}
+    {# For a running job this reflects the time elapsed so far; the container refreshes every 5s #}
+    {% with execution_time=job.elapsed_time|humanize_duration %}
+      {% if execution_time %}
+        {% trans "Execution time" %}:
+        <strong{% if not job.completed %} class="text-primary" title="{% trans "Still running" %}"{% endif %}>{{ execution_time }}</strong>
+      {% endif %}
+    {% endwith %}
     <span id="pending-result-label">{% badge job.get_status_display job.get_status_color %}</span>
   </p>
   {% if job.completed %}

+ 14 - 4
netbox/utilities/string.py

@@ -11,15 +11,25 @@ __all__ = (
 
 def humanize_duration(value):
     """
-    Express a timedelta in a human-friendly format. Example: 1h 5m 23s. Returns an empty string
-    for None; zero-duration timedeltas render as "0s".
+    Express a timedelta in a human-friendly format. Example: 1h 5m 23s. Sub-second durations are
+    rendered with millisecond precision (e.g. 0.43s). Returns an empty string for None; zero and
+    negative durations render as "0s".
     """
     if value is None:
         return ''
 
+    # Negative durations (which can result from clock skew) are clamped to zero
+    total_seconds = max(value.total_seconds(), 0)
+
+    # Render sub-second durations with millisecond precision, as rounding them to whole seconds
+    # would report every short-lived duration as zero. Trailing zeros are stripped.
+    if 0 < total_seconds < 1:
+        milliseconds = f'{total_seconds:.3f}'.rstrip('0').rstrip('.')
+        if milliseconds != '0':
+            return f'{milliseconds}s'
+
     # Round to whole seconds and decompose
-    total_seconds = int(value.total_seconds())
-    days, remainder = divmod(total_seconds, 86400)
+    days, remainder = divmod(int(total_seconds), 86400)
     hours, remainder = divmod(remainder, 3600)
     minutes, seconds = divmod(remainder, 60)
 

+ 17 - 0
netbox/utilities/templatetags/helpers.py

@@ -17,6 +17,7 @@ from netbox.ui.attrs import (
 )
 from utilities.forms import TableConfigForm, get_selected_values
 from utilities.forms.mixins import FORM_FIELD_LOOKUPS
+from utilities.string import humanize_duration
 from utilities.views import get_action_url, get_viewname
 
 __all__ = (
@@ -31,6 +32,7 @@ __all__ = (
     'get_item',
     'get_key',
     'humanize_disk_capacity',
+    'humanize_duration_filter',
     'humanize_ram_capacity',
     'humanize_speed',
     'icon_from_status',
@@ -211,6 +213,21 @@ def _format_speed(speed, divisor, unit):
     return f'{whole}.{fraction} {unit}'
 
 
+@register.filter('humanize_duration')
+def humanize_duration_filter(value):
+    """
+    Express a timedelta in a human-friendly format, always using the largest appropriate units.
+    Sub-second durations are rendered with millisecond precision.
+
+    Examples:
+
+        timedelta(seconds=90) => "1m 30s"
+        timedelta(hours=1, minutes=5, seconds=23) => "1h 5m 23s"
+        timedelta(milliseconds=430) => "0.43s"
+    """
+    return humanize_duration(value)
+
+
 @register.filter()
 def humanize_speed(speed):
     """

+ 17 - 3
netbox/utilities/tests/test_string.py

@@ -28,6 +28,20 @@ class HumanizeDurationTest(TestCase):
     def test_whole_minute_omits_seconds(self):
         self.assertEqual(humanize_duration(timedelta(minutes=2)), '2m')
 
-    def test_sub_second_rounds_down_to_zero(self):
-        # Fractional seconds are truncated; a sub-second duration reads as 0s.
-        self.assertEqual(humanize_duration(timedelta(milliseconds=500)), '0s')
+    def test_sub_second_renders_decimal(self):
+        # Sub-second durations retain millisecond precision, with trailing zeros stripped.
+        self.assertEqual(humanize_duration(timedelta(milliseconds=500)), '0.5s')
+        self.assertEqual(humanize_duration(timedelta(milliseconds=430)), '0.43s')
+        self.assertEqual(humanize_duration(timedelta(milliseconds=4)), '0.004s')
+
+    def test_sub_millisecond_rounds_to_zero(self):
+        # Anything below a millisecond has no decimal representation, so it reads as 0s.
+        self.assertEqual(humanize_duration(timedelta(microseconds=400)), '0s')
+
+    def test_fractional_seconds_truncated_above_one_second(self):
+        self.assertEqual(humanize_duration(timedelta(seconds=1, milliseconds=999)), '1s')
+
+    def test_negative_duration_clamped_to_zero(self):
+        # A negative duration (e.g. resulting from clock skew) never renders as negative.
+        self.assertEqual(humanize_duration(timedelta(seconds=-1.5)), '0s')
+        self.assertEqual(humanize_duration(timedelta(days=-2)), '0s')