Kaynağa Gözat

Closes #15618: Always use ISO 8601 date & time formatting (#15737)

* Introduce the isodate(), isotime(), and isodatetime() template filters

* Display the relative time on mouse hover

* Render journal entry times in ISO 8601 format

* Use ISO 8601 format when displaying dates & times in a table

* Standardize the use of DateTimeColumn across all tables
Jeremy Stretch 1 yıl önce
ebeveyn
işleme
77a4300888

+ 4 - 2
netbox/account/tables.py

@@ -30,10 +30,12 @@ class UserTokenTable(NetBoxTable):
     write_enabled = columns.BooleanColumn(
         verbose_name=_('Write Enabled')
     )
-    created = columns.DateColumn(
+    created = columns.DateTimeColumn(
+        timespec='minutes',
         verbose_name=_('Created'),
     )
-    expires = columns.DateColumn(
+    expires = columns.DateTimeColumn(
+        timespec='minutes',
         verbose_name=_('Expires'),
     )
     last_used = columns.DateTimeColumn(

+ 5 - 5
netbox/core/tables/tasks.py

@@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _
 from django_tables2.utils import A
 
 from core.tables.columns import RQJobStatusColumn
-from netbox.tables import BaseTable
+from netbox.tables import BaseTable, columns
 
 
 class BackgroundQueueTable(BaseTable):
@@ -75,13 +75,13 @@ class BackgroundTaskTable(BaseTable):
         linkify=("core:background_task", [A("id")]),
         verbose_name=_("ID")
     )
-    created_at = tables.DateTimeColumn(
+    created_at = columns.DateTimeColumn(
         verbose_name=_("Created")
     )
-    enqueued_at = tables.DateTimeColumn(
+    enqueued_at = columns.DateTimeColumn(
         verbose_name=_("Enqueued")
     )
-    ended_at = tables.DateTimeColumn(
+    ended_at = columns.DateTimeColumn(
         verbose_name=_("Ended")
     )
     status = RQJobStatusColumn(
@@ -117,7 +117,7 @@ class WorkerTable(BaseTable):
     state = tables.Column(
         verbose_name=_("State")
     )
-    birth_date = tables.DateTimeColumn(
+    birth_date = columns.DateTimeColumn(
         verbose_name=_("Birth")
     )
     pid = tables.Column(

+ 1 - 1
netbox/extras/models/models.py

@@ -732,7 +732,7 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat
 
     def __str__(self):
         created = timezone.localtime(self.created)
-        return f"{date_format(created, format='SHORT_DATETIME_FORMAT')} ({self.get_kind_display()})"
+        return f"{created.date().isoformat()} {created.time().isoformat(timespec='minutes')} ({self.get_kind_display()})"
 
     def get_absolute_url(self):
         return reverse('extras:journalentry', args=[self.pk])

+ 6 - 6
netbox/extras/tables/tables.py

@@ -432,10 +432,10 @@ class ConfigTemplateTable(NetBoxTable):
 
 
 class ObjectChangeTable(NetBoxTable):
-    time = tables.DateTimeColumn(
+    time = columns.DateTimeColumn(
         verbose_name=_('Time'),
-        linkify=True,
-        format=settings.SHORT_DATETIME_FORMAT
+        timespec='minutes',
+        linkify=True
     )
     user_name = tables.Column(
         verbose_name=_('Username')
@@ -475,10 +475,10 @@ class ObjectChangeTable(NetBoxTable):
 
 
 class JournalEntryTable(NetBoxTable):
-    created = tables.DateTimeColumn(
+    created = columns.DateTimeColumn(
         verbose_name=_('Created'),
-        linkify=True,
-        format=settings.SHORT_DATETIME_FORMAT
+        timespec='minutes',
+        linkify=True
     )
     assigned_object_type = columns.ContentTypeColumn(
         verbose_name=_('Object Type')

+ 20 - 14
netbox/netbox/tables/columns.py

@@ -10,7 +10,6 @@ from django.db.models import DateField, DateTimeField
 from django.template import Context, Template
 from django.urls import reverse
 from django.utils.dateparse import parse_date
-from django.utils.formats import date_format
 from django.utils.html import escape
 from django.utils.safestring import mark_safe
 from django.utils.translation import gettext_lazy as _
@@ -52,18 +51,17 @@ __all__ = (
 #
 
 @library.register
-class DateColumn(tables.DateColumn):
+class DateColumn(tables.Column):
     """
-    Overrides the default implementation of DateColumn to better handle null values, returning a default value for
-    tables and null when exporting data. It is registered in the tables library to use this class instead of the
-    default, making this behavior consistent in all fields of type DateField.
+    Render a datetime.date in ISO 8601 format.
     """
     def render(self, value):
         if value:
-            return date_format(value, format="SHORT_DATE_FORMAT")
+            return value.isoformat()
 
     def value(self, value):
-        return value
+        if value:
+            return value.isoformat()
 
     @classmethod
     def from_field(cls, field, **kwargs):
@@ -72,16 +70,24 @@ class DateColumn(tables.DateColumn):
 
 
 @library.register
-class DateTimeColumn(tables.DateTimeColumn):
+class DateTimeColumn(tables.Column):
     """
-    Overrides the default implementation of DateTimeColumn to better handle null values, returning a default value for
-    tables and null when exporting data. It is registered in the tables library to use this class instead of the
-    default, making this behavior consistent in all fields of type DateTimeField.
+    Render a datetime.datetime in ISO 8601 format.
+
+    Args:
+        timespec: Granularity specification; passed through to datetime.isoformat()
     """
+    def __init__(self, *args, timespec='seconds', **kwargs):
+        self.timespec = timespec
+        super().__init__(*args, **kwargs)
+
+    def render(self, value):
+        if value:
+            return f"{value.date().isoformat()} {value.time().isoformat(timespec=self.timespec)}"
+
     def value(self, value):
         if value:
-            return date_format(value, format="SHORT_DATETIME_FORMAT")
-        return None
+            return value.isoformat()
 
     @classmethod
     def from_field(cls, field, **kwargs):
@@ -498,7 +504,7 @@ class CustomFieldColumn(tables.Column):
         if self.customfield.type == CustomFieldTypeChoices.TYPE_LONGTEXT and value:
             return render_markdown(value)
         if self.customfield.type == CustomFieldTypeChoices.TYPE_DATE and value:
-            return date_format(parse_date(value), format="SHORT_DATE_FORMAT")
+            return parse_date(value).isoformat()
         if value is not None:
             obj = self.customfield.deserialize(value)
             return mark_safe(self._linkify_item(obj))

+ 2 - 2
netbox/templates/account/profile.html

@@ -31,11 +31,11 @@
           </tr>
           <tr>
             <th scope="row">{% trans "Account Created" %}</th>
-            <td>{{ request.user.date_joined|annotated_date }}</td>
+            <td>{{ request.user.date_joined|isodate }}</td>
           </tr>
           <tr>
             <th scope="row">{% trans "Last Login" %}</th>
-            <td>{{ request.user.last_login|annotated_date }}</td>
+            <td>{{ request.user.last_login|isodatetime:"minutes"|placeholder }}</td>
           </tr>
           <tr>
             <th scope="row">{% trans "Superuser" %}</th>

+ 3 - 3
netbox/templates/account/token.html

@@ -41,15 +41,15 @@
           </tr>
           <tr>
             <th scope="row">{% trans "Created" %}</th>
-            <td>{{ object.created|annotated_date }}</td>
+            <td>{{ object.created|isodatetime }}</td>
           </tr>
           <tr>
             <th scope="row">{% trans "Expires" %}</th>
-            <td>{{ object.expires|placeholder }}</td>
+            <td>{{ object.expires|isodatetime|placeholder }}</td>
           </tr>
           <tr>
             <th scope="row">{% trans "Last used" %}</th>
-            <td>{{ object.last_used|placeholder }}</td>
+            <td>{{ object.last_used|isodatetime|placeholder }}</td>
           </tr>
           <tr>
             <th scope="row">{% trans "Allowed IPs" %}</th>

+ 2 - 2
netbox/templates/circuits/circuit.html

@@ -45,11 +45,11 @@
           </tr>
           <tr>
             <th scope="row">{% trans "Install Date" %}</th>
-            <td>{{ object.install_date|annotated_date|placeholder }}</td>
+            <td>{{ object.install_date|isodate|placeholder }}</td>
           </tr>
           <tr>
             <th scope="row">{% trans "Termination Date" %}</th>
-            <td>{{ object.termination_date|annotated_date|placeholder }}</td>
+            <td>{{ object.termination_date|isodate|placeholder }}</td>
           </tr>
           <tr>
             <th scope="row">{% trans "Commit Rate" %}</th>

+ 1 - 1
netbox/templates/core/configrevision.html

@@ -23,7 +23,7 @@
 {% block subtitle %}
   {% if object.created %}
     <div class="text-secondary fs-5">
-      {% trans "Created" %} {{ object.created|annotated_date }}
+      {% trans "Created" %} {{ object.created|isodatetime }}
     </div>
   {% endif %}
 {% endblock subtitle %}

+ 1 - 1
netbox/templates/core/configrevision_restore.html

@@ -9,7 +9,7 @@
 
 {% block subtitle %}
   <div class="text-secondary fs-5">
-    {% trans "Created" %} {{ object.created|annotated_date }}
+    {% trans "Created" %} {{ object.created|isodatetime }}
   </div>
 {% endblock %}
 

+ 4 - 4
netbox/templates/core/job.html

@@ -49,12 +49,12 @@
         <table class="table table-hover attr-table">
           <tr>
             <th scope="row">{% trans "Created" %}</th>
-            <td>{{ object.created|annotated_date }}</td>
+            <td>{{ object.created|isodatetime }}</td>
           </tr>
           <tr>
             <th scope="row">{% trans "Scheduled" %}</th>
             <td>
-              {{ object.scheduled|annotated_date|placeholder }}
+              {{ object.scheduled|isodatetime|placeholder }}
               {% if object.interval %}
                 ({% blocktrans with interval=object.interval %}every {{ interval }} minutes{% endblocktrans %})
               {% endif %}
@@ -62,11 +62,11 @@
           </tr>
           <tr>
             <th scope="row">{% trans "Started" %}</th>
-            <td>{{ object.started|annotated_date|placeholder }}</td>
+            <td>{{ object.started|isodatetime|placeholder }}</td>
           </tr>
           <tr>
             <th scope="row">{% trans "Completed" %}</th>
-            <td>{{ object.completed|annotated_date|placeholder }}</td>
+            <td>{{ object.completed|isodatetime|placeholder }}</td>
           </tr>
         </table>
       </div>

+ 3 - 3
netbox/templates/core/rq_task.html

@@ -13,7 +13,7 @@
 
 {% block subtitle %}
   <div class="text-secondary fs-5">
-    <span>{% trans "Created" %} {{ job.created_at|annotated_date }}</span>
+    <span>{% trans "Created" %} {{ job.created_at|isodatetime }}</span>
   </div>
 {% endblock subtitle %}
 
@@ -71,11 +71,11 @@
           </tr>
           <tr>
             <th scope="row">{% trans "Created" %}</th>
-            <td>{{ job.created_at|annotated_date }}</td>
+            <td>{{ job.created_at|isodatetime }}</td>
           </tr>
           <tr>
             <th scope="row">{% trans "Queued" %}</th>
-            <td>{{ job.enqueued_at|annotated_date }}</td>
+            <td>{{ job.enqueued_at|isodatetime }}</td>
           </tr>
           <tr>
             <th scope="row">{% trans "Status" %}</th>

+ 2 - 2
netbox/templates/core/rq_worker.html

@@ -11,7 +11,7 @@
 
 {% block subtitle %}
   <div class="text-secondary fs-5">
-    <span>{% trans "Created" %} {{ worker.birth_date|annotated_date }}</span>
+    <span>{% trans "Created" %} {{ worker.birth_date|isodatetime }}</span>
   </div>
 {% endblock subtitle %}
 
@@ -49,7 +49,7 @@
             </tr>
             <tr>
               <th scope="row">{% trans "Birth" %}</th>
-              <td>{{ worker.birth_date|annotated_date }}</td>
+              <td>{{ worker.birth_date|isodatetime }}</td>
             </tr>
             <tr>
               <th scope="row">{% trans "Queues" %}</th>

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

@@ -1,4 +1,3 @@
-{% load humanize %}
 {% load helpers %}
 {% load log_levels %}
 {% load i18n %}
@@ -6,11 +5,11 @@
 <div class="htmx-container">
   <p>
     {% if job.started %}
-      {% trans "Started" %}: <strong>{{ job.started|annotated_date }}</strong>
+      {% trans "Started" %}: <strong>{{ job.started|isodatetime }}</strong>
     {% elif job.scheduled %}
-      {% trans "Scheduled for" %}: <strong>{{ job.scheduled|annotated_date }}</strong> ({{ job.scheduled|naturaltime }})
+      {% trans "Scheduled for" %}: <strong>{{ job.scheduled|isodatetime }}</strong>
     {% else %}
-      {% trans "Created" %}: <strong>{{ job.created|annotated_date }}</strong>
+      {% trans "Created" %}: <strong>{{ job.created|isodatetime }}</strong>
     {% endif %}
     {% if job.completed %}
       {% trans "Duration" %}: <strong>{{ job.duration }}</strong>

+ 1 - 1
netbox/templates/extras/journalentry.html

@@ -20,7 +20,7 @@
                     </tr>
                     <tr>
                         <th scope="row">{% trans "Created" %}</th>
-                        <td>{{ object.created|annotated_date }}</td>
+                        <td>{{ object.created|isodatetime:"minutes" }}</td>
                     </tr>
                     <tr>
                         <th scope="row">{% trans "Created By" %}</th>

+ 1 - 3
netbox/templates/extras/objectchange.html

@@ -29,9 +29,7 @@
             <table class="table table-hover attr-table">
                 <tr>
                     <th scope="row">{% trans "Time" %}</th>
-                    <td>
-                        {{ object.time|annotated_date }}
-                    </td>
+                    <td>{{ object.time|isodatetime }}</td>
                 </tr>
                 <tr>
                     <th scope="row">{% trans "User" %}</th>

+ 1 - 1
netbox/templates/extras/script_list.html

@@ -67,7 +67,7 @@
                   <td>{{ script.description|markdown|placeholder }}</td>
                   {% if last_job %}
                     <td>
-                      <a href="{% url 'extras:script_result' job_pk=last_job.pk %}">{{ last_job.created|annotated_date }}</a>
+                      <a href="{% url 'extras:script_result' job_pk=last_job.pk %}">{{ last_job.created|isodatetime }}</a>
                     </td>
                     <td>
                       {% badge last_job.get_status_display last_job.get_status_color %}

+ 1 - 1
netbox/templates/extras/script_result.html

@@ -17,7 +17,7 @@
         <li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}">{% trans "Scripts" %}</a></li>
         <li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}#module.{{ script.module }}">{{ script.module|bettertitle }}</a></li>
         <li class="breadcrumb-item"><a href="{{ script.get_absolute_url }}">{{ script }}</a></li>
-        <li class="breadcrumb-item">{{ job.created|annotated_date }}</li>
+        <li class="breadcrumb-item">{{ job.created|isodatetime }}</li>
       </ol>
     </nav>
   </div>

+ 2 - 2
netbox/templates/generic/object.html

@@ -48,10 +48,10 @@ Context:
 
 {% block subtitle %}
   <div class="text-secondary fs-5">
-    <span>{% trans "Created" %} {{ object.created|annotated_date }}</span>
+    {% trans "Created" %} {{ object.created|isodatetime:"minutes" }}
     {% if object.last_updated %}
       <span class="separator">&middot;</span>
-      <span>{% trans "Updated" %} <span title="{{ object.last_updated }}">{{ object.last_updated|timesince }}</span> {% trans "ago" %}</span>
+      {% trans "Updated" %} {{ object.last_updated|isodatetime:"minutes" }}
     {% endif %}
   </div>
 {% endblock subtitle %}

+ 1 - 1
netbox/templates/ipam/aggregate.html

@@ -37,7 +37,7 @@
           </tr>
           <tr>
             <th scope="row">{% trans "Date Added" %}</th>
-            <td>{{ object.date_added|annotated_date|placeholder }}</td>
+            <td>{{ object.date_added|isodate|placeholder }}</td>
           </tr>
           <tr>
             <th scope="row">{% trans "Description" %}</th>

+ 4 - 4
netbox/templates/users/token.html

@@ -1,6 +1,6 @@
 {% extends 'generic/object.html' %}
-{% load i18n %}
 {% load helpers %}
+{% load i18n %}
 {% load render_table from django_tables2 %}
 
 {% block title %}{% trans "Token" %} {{ object }}{% endblock %}
@@ -33,15 +33,15 @@
           </tr>
           <tr>
             <th scope="row">{% trans "Created" %}</th>
-            <td>{{ object.created|annotated_date }}</td>
+            <td>{{ object.created|isodatetime }}</td>
           </tr>
           <tr>
             <th scope="row">{% trans "Expires" %}</th>
-            <td>{{ object.expires|placeholder }}</td>
+            <td>{{ object.expires|isodatetime|placeholder }}</td>
           </tr>
           <tr>
             <th scope="row">{% trans "Last used" %}</th>
-            <td>{{ object.last_used|placeholder }}</td>
+            <td>{{ object.last_used|isodatetime|placeholder }}</td>
           </tr>
           <tr>
             <th scope="row">{% trans "Allowed IPs" %}</th>

+ 2 - 2
netbox/templates/users/user.html

@@ -27,11 +27,11 @@
           </tr>
           <tr>
             <th scope="row">{% trans "Account Created" %}</th>
-            <td>{{ object.date_joined|annotated_date }}</td>
+            <td>{{ object.date_joined|isodate }}</td>
           </tr>
           <tr>
             <th scope="row">{% trans "Last Login" %}</th>
-            <td>{{ object.last_login|annotated_date }}</td>
+            <td>{{ object.last_login|isodatetime:"minutes"|placeholder }}</td>
           </tr>
           <tr>
             <th scope="row">{% trans "Active" %}</th>

+ 2 - 2
netbox/utilities/templates/builtins/customfield_value.html

@@ -9,9 +9,9 @@
 {% elif customfield.type == 'boolean' and value == False %}
   {% checkmark value false="False" %}
 {% elif customfield.type == 'date' and value %}
-  {{ value|annotated_date }}
+  {{ value|isodate }}
 {% elif customfield.type == 'datetime' and value %}
-  {{ value|annotated_date }}
+  {{ value|isodate }} {{ value|isodatetime }}
 {% elif customfield.type == 'url' and value %}
   <a href="{{ value }}">{{ value|truncatechars:70 }}</a>
 {% elif customfield.type == 'json' and value %}

+ 37 - 0
netbox/utilities/templatetags/builtins/filters.py

@@ -5,6 +5,7 @@ import re
 import yaml
 from django import template
 from django.contrib.contenttypes.models import ContentType
+from django.contrib.humanize.templatetags.humanize import naturaltime
 from django.utils.html import escape
 from django.utils.safestring import mark_safe
 from markdown import markdown
@@ -20,6 +21,9 @@ __all__ = (
     'content_type',
     'content_type_id',
     'fgcolor',
+    'isodate',
+    'isodatetime',
+    'isotime',
     'linkify',
     'meta',
     'placeholder',
@@ -202,3 +206,36 @@ def render_yaml(value):
         {{ data_dict|yaml }}
     """
     return yaml.dump(json.loads(json.dumps(value)))
+
+
+#
+# Time & date
+#
+
+@register.filter()
+def isodate(value):
+    if type(value) is datetime.date:
+        text = value.isoformat()
+    elif type(value) is datetime.datetime:
+        text = value.date().isoformat()
+    else:
+        return ''
+    return mark_safe(f'<span title="{naturaltime(value)}">{text}</span>')
+
+
+@register.filter()
+def isotime(value, spec='seconds'):
+    if type(value) is datetime.time:
+        return value.isoformat(timespec=spec)
+    if type(value) is datetime.datetime:
+        return value.time().isoformat(timespec=spec)
+    return ''
+
+
+@register.filter()
+def isodatetime(value, spec='seconds'):
+    if type(value) is datetime.datetime:
+        text = f'{isodate(value)} {isotime(value, spec=spec)}'
+    else:
+        return ''
+    return mark_safe(f'<span title="{naturaltime(value)}">{text}</span>')