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
This commit is contained in:
Jeremy Stretch 2024-04-17 11:46:47 -04:00 committed by GitHub
parent f0aca5bac1
commit 77a4300888
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 109 additions and 67 deletions

View File

@ -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(

View File

@ -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(

View File

@ -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])

View File

@ -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')

View File

@ -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))

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 %}

View File

@ -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>')