From 77a4300888e80e95791b45f8a05a082999168a23 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 17 Apr 2024 11:46:47 -0400 Subject: [PATCH] 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 --- netbox/account/tables.py | 6 ++- netbox/core/tables/tasks.py | 10 ++--- netbox/extras/models/models.py | 2 +- netbox/extras/tables/tables.py | 12 +++--- netbox/netbox/tables/columns.py | 34 ++++++++++------- netbox/templates/account/profile.html | 4 +- netbox/templates/account/token.html | 6 +-- netbox/templates/circuits/circuit.html | 4 +- netbox/templates/core/configrevision.html | 2 +- .../core/configrevision_restore.html | 2 +- netbox/templates/core/job.html | 8 ++-- netbox/templates/core/rq_task.html | 6 +-- netbox/templates/core/rq_worker.html | 4 +- .../templates/extras/htmx/script_result.html | 7 ++-- netbox/templates/extras/journalentry.html | 2 +- netbox/templates/extras/objectchange.html | 4 +- netbox/templates/extras/script_list.html | 2 +- netbox/templates/extras/script_result.html | 2 +- netbox/templates/generic/object.html | 4 +- netbox/templates/ipam/aggregate.html | 2 +- netbox/templates/users/token.html | 8 ++-- netbox/templates/users/user.html | 4 +- .../templates/builtins/customfield_value.html | 4 +- .../templatetags/builtins/filters.py | 37 +++++++++++++++++++ 24 files changed, 109 insertions(+), 67 deletions(-) diff --git a/netbox/account/tables.py b/netbox/account/tables.py index 6655a7f82..bcc0a0ccd 100644 --- a/netbox/account/tables.py +++ b/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( diff --git a/netbox/core/tables/tasks.py b/netbox/core/tables/tasks.py index 531ec6375..f53e598b5 100644 --- a/netbox/core/tables/tasks.py +++ b/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( diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 16f10b485..49249eaa0 100644 --- a/netbox/extras/models/models.py +++ b/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]) diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index a0f504931..04bebd603 100644 --- a/netbox/extras/tables/tables.py +++ b/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') diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index 193bf8a17..c37bb1b0d 100644 --- a/netbox/netbox/tables/columns.py +++ b/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)) diff --git a/netbox/templates/account/profile.html b/netbox/templates/account/profile.html index 53122168f..f494df910 100644 --- a/netbox/templates/account/profile.html +++ b/netbox/templates/account/profile.html @@ -31,11 +31,11 @@ {% trans "Account Created" %} - {{ request.user.date_joined|annotated_date }} + {{ request.user.date_joined|isodate }} {% trans "Last Login" %} - {{ request.user.last_login|annotated_date }} + {{ request.user.last_login|isodatetime:"minutes"|placeholder }} {% trans "Superuser" %} diff --git a/netbox/templates/account/token.html b/netbox/templates/account/token.html index c65d80cb9..7001d6a14 100644 --- a/netbox/templates/account/token.html +++ b/netbox/templates/account/token.html @@ -41,15 +41,15 @@ {% trans "Created" %} - {{ object.created|annotated_date }} + {{ object.created|isodatetime }} {% trans "Expires" %} - {{ object.expires|placeholder }} + {{ object.expires|isodatetime|placeholder }} {% trans "Last used" %} - {{ object.last_used|placeholder }} + {{ object.last_used|isodatetime|placeholder }} {% trans "Allowed IPs" %} diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html index 69284f9b4..fb3d8185a 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -45,11 +45,11 @@ {% trans "Install Date" %} - {{ object.install_date|annotated_date|placeholder }} + {{ object.install_date|isodate|placeholder }} {% trans "Termination Date" %} - {{ object.termination_date|annotated_date|placeholder }} + {{ object.termination_date|isodate|placeholder }} {% trans "Commit Rate" %} diff --git a/netbox/templates/core/configrevision.html b/netbox/templates/core/configrevision.html index 34ee6bda9..1be674ab4 100644 --- a/netbox/templates/core/configrevision.html +++ b/netbox/templates/core/configrevision.html @@ -23,7 +23,7 @@ {% block subtitle %} {% if object.created %}
- {% trans "Created" %} {{ object.created|annotated_date }} + {% trans "Created" %} {{ object.created|isodatetime }}
{% endif %} {% endblock subtitle %} diff --git a/netbox/templates/core/configrevision_restore.html b/netbox/templates/core/configrevision_restore.html index 0097f93e5..ee0ce1ebe 100644 --- a/netbox/templates/core/configrevision_restore.html +++ b/netbox/templates/core/configrevision_restore.html @@ -9,7 +9,7 @@ {% block subtitle %}
- {% trans "Created" %} {{ object.created|annotated_date }} + {% trans "Created" %} {{ object.created|isodatetime }}
{% endblock %} diff --git a/netbox/templates/core/job.html b/netbox/templates/core/job.html index 0851fa114..1c67791b8 100644 --- a/netbox/templates/core/job.html +++ b/netbox/templates/core/job.html @@ -49,12 +49,12 @@ - + - + - +
{% trans "Created" %}{{ object.created|annotated_date }}{{ object.created|isodatetime }}
{% trans "Scheduled" %} - {{ 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 @@
{% trans "Started" %}{{ object.started|annotated_date|placeholder }}{{ object.started|isodatetime|placeholder }}
{% trans "Completed" %}{{ object.completed|annotated_date|placeholder }}{{ object.completed|isodatetime|placeholder }}
diff --git a/netbox/templates/core/rq_task.html b/netbox/templates/core/rq_task.html index 68829e258..d84bc5a3f 100644 --- a/netbox/templates/core/rq_task.html +++ b/netbox/templates/core/rq_task.html @@ -13,7 +13,7 @@ {% block subtitle %}
- {% trans "Created" %} {{ job.created_at|annotated_date }} + {% trans "Created" %} {{ job.created_at|isodatetime }}
{% endblock subtitle %} @@ -71,11 +71,11 @@ {% trans "Created" %} - {{ job.created_at|annotated_date }} + {{ job.created_at|isodatetime }} {% trans "Queued" %} - {{ job.enqueued_at|annotated_date }} + {{ job.enqueued_at|isodatetime }} {% trans "Status" %} diff --git a/netbox/templates/core/rq_worker.html b/netbox/templates/core/rq_worker.html index d44f00b66..4dc4bf91d 100644 --- a/netbox/templates/core/rq_worker.html +++ b/netbox/templates/core/rq_worker.html @@ -11,7 +11,7 @@ {% block subtitle %}
- {% trans "Created" %} {{ worker.birth_date|annotated_date }} + {% trans "Created" %} {{ worker.birth_date|isodatetime }}
{% endblock subtitle %} @@ -49,7 +49,7 @@ {% trans "Birth" %} - {{ worker.birth_date|annotated_date }} + {{ worker.birth_date|isodatetime }} {% trans "Queues" %} diff --git a/netbox/templates/extras/htmx/script_result.html b/netbox/templates/extras/htmx/script_result.html index e532e07e1..0545e2bd5 100644 --- a/netbox/templates/extras/htmx/script_result.html +++ b/netbox/templates/extras/htmx/script_result.html @@ -1,4 +1,3 @@ -{% load humanize %} {% load helpers %} {% load log_levels %} {% load i18n %} @@ -6,11 +5,11 @@

{% if job.started %} - {% trans "Started" %}: {{ job.started|annotated_date }} + {% trans "Started" %}: {{ job.started|isodatetime }} {% elif job.scheduled %} - {% trans "Scheduled for" %}: {{ job.scheduled|annotated_date }} ({{ job.scheduled|naturaltime }}) + {% trans "Scheduled for" %}: {{ job.scheduled|isodatetime }} {% else %} - {% trans "Created" %}: {{ job.created|annotated_date }} + {% trans "Created" %}: {{ job.created|isodatetime }} {% endif %} {% if job.completed %} {% trans "Duration" %}: {{ job.duration }} diff --git a/netbox/templates/extras/journalentry.html b/netbox/templates/extras/journalentry.html index 7a4c06b25..29c0a263c 100644 --- a/netbox/templates/extras/journalentry.html +++ b/netbox/templates/extras/journalentry.html @@ -20,7 +20,7 @@ {% trans "Created" %} - {{ object.created|annotated_date }} + {{ object.created|isodatetime:"minutes" }} {% trans "Created By" %} diff --git a/netbox/templates/extras/objectchange.html b/netbox/templates/extras/objectchange.html index 110aeeb99..0aee6185b 100644 --- a/netbox/templates/extras/objectchange.html +++ b/netbox/templates/extras/objectchange.html @@ -29,9 +29,7 @@ - + diff --git a/netbox/templates/extras/script_list.html b/netbox/templates/extras/script_list.html index dc7a168f8..7ce5ca6eb 100644 --- a/netbox/templates/extras/script_list.html +++ b/netbox/templates/extras/script_list.html @@ -67,7 +67,7 @@ {% if last_job %} - + diff --git a/netbox/templates/users/token.html b/netbox/templates/users/token.html index 2d1858323..968c1d421 100644 --- a/netbox/templates/users/token.html +++ b/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 @@ - + - + - + diff --git a/netbox/templates/users/user.html b/netbox/templates/users/user.html index 0dd12fb52..c708fb963 100644 --- a/netbox/templates/users/user.html +++ b/netbox/templates/users/user.html @@ -27,11 +27,11 @@ - + - + diff --git a/netbox/utilities/templates/builtins/customfield_value.html b/netbox/utilities/templates/builtins/customfield_value.html index 6091fd411..462e62b86 100644 --- a/netbox/utilities/templates/builtins/customfield_value.html +++ b/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 %} {{ value|truncatechars:70 }} {% elif customfield.type == 'json' and value %} diff --git a/netbox/utilities/templatetags/builtins/filters.py b/netbox/utilities/templatetags/builtins/filters.py index 3d7cfec2f..842f7b14a 100644 --- a/netbox/utilities/templatetags/builtins/filters.py +++ b/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'{text}') + + +@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'{text}')
{% trans "Time" %} - {{ object.time|annotated_date }} - {{ object.time|isodatetime }}
{% trans "User" %}{{ script.description|markdown|placeholder }} - {{ last_job.created|annotated_date }} + {{ last_job.created|isodatetime }} {% badge last_job.get_status_display last_job.get_status_color %} diff --git a/netbox/templates/extras/script_result.html b/netbox/templates/extras/script_result.html index 276e2562a..ee1127c8a 100644 --- a/netbox/templates/extras/script_result.html +++ b/netbox/templates/extras/script_result.html @@ -17,7 +17,7 @@ - + diff --git a/netbox/templates/generic/object.html b/netbox/templates/generic/object.html index f111c20ef..112368870 100644 --- a/netbox/templates/generic/object.html +++ b/netbox/templates/generic/object.html @@ -48,10 +48,10 @@ Context: {% block subtitle %}
- {% trans "Created" %} {{ object.created|annotated_date }} + {% trans "Created" %} {{ object.created|isodatetime:"minutes" }} {% if object.last_updated %} · - {% trans "Updated" %} {{ object.last_updated|timesince }} {% trans "ago" %} + {% trans "Updated" %} {{ object.last_updated|isodatetime:"minutes" }} {% endif %}
{% endblock subtitle %} diff --git a/netbox/templates/ipam/aggregate.html b/netbox/templates/ipam/aggregate.html index 6819e5448..a6415463e 100644 --- a/netbox/templates/ipam/aggregate.html +++ b/netbox/templates/ipam/aggregate.html @@ -37,7 +37,7 @@
{% trans "Date Added" %}{{ object.date_added|annotated_date|placeholder }}{{ object.date_added|isodate|placeholder }}
{% trans "Description" %}
{% trans "Created" %}{{ object.created|annotated_date }}{{ object.created|isodatetime }}
{% trans "Expires" %}{{ object.expires|placeholder }}{{ object.expires|isodatetime|placeholder }}
{% trans "Last used" %}{{ object.last_used|placeholder }}{{ object.last_used|isodatetime|placeholder }}
{% trans "Allowed IPs" %}
{% trans "Account Created" %}{{ object.date_joined|annotated_date }}{{ object.date_joined|isodate }}
{% trans "Last Login" %}{{ object.last_login|annotated_date }}{{ object.last_login|isodatetime:"minutes"|placeholder }}
{% trans "Active" %}